TrafficServerLocal/README.md
merios 1849404dd1 docs: rationale de la licencia GPL-3.0
Documenta por que el laboratorio eligio GPL-3.0 en lugar de AGPL-3.0:
copyleft estricto en la distribucion, libertad total en el uso. La
decision queda registrada para que no se revise sin consultar el criterio.
2026-10-15 11:49:21 -03:00

471 lines
15 KiB
Markdown

# TrafficServerLocal
Infraestructura base para alojar múltiples proyectos Docker detrás de un único
proxy inverso **Traefik v3**, con soporte para **HTTP y HTTPS**:
- **Dominios públicos** → certificados de **Let's Encrypt** (automático).
- **Dominios locales `.local`** → certificados **auto-firmados** montados desde
el filesystem.
Este repositorio contiene **solo la infraestructura reutilizable**. El código,
las bases de datos y los datos persistentes de cada proyecto viven en
`~/srv/projects/<proyecto>/` y se publican en sus propios repositorios.
---
## 1. Estructura
```text
~/srv/
├── core/ # Reverse proxy Traefik
│ ├── docker-compose.yml
│ └── data/
│ ├── traefik.yml # Configuración estática
│ ├── dynamic_tls.yml # Certificados estáticos (.local)
│ ├── acme.json # Clave ACME (NO versionado)
│ └── certs/ # .crt / .key locales (NO versionados)
├── projects/ # Un directorio por proyecto
│ └── <proyecto>/
│ ├── app/ # Código fuente / Dockerfile
│ ├── data/{db,storage}/ # Persistencia
│ ├── .env # Secretos (chmod 600)
│ └── docker-compose.yml # Etiquetas de Traefik
└── backups/ # Respaldos (fuera de Git)
```
En este repositorio (lo publicable):
```text
TrafficServerLocal/
├── README.md
├── LICENSE # GPL-3.0
├── .gitignore
├── .env.global
├── init_project.sh # Generador de proyectos base
├── docs/
│ ├── arquitectura.md # Guía de instalación y gestión de URLs
│ ├── http-https.md # Referencia de etiquetas HTTP/HTTPS y TLS
│ └── publicacion-anonima.md # Política de publicación anonimizada
├── tools/
│ └── verificar-anonimato.sh # Chequea PII antes de publicar
└── core/
├── docker-compose.yml
└── data/
├── traefik.yml
├── dynamic_tls.yml
└── certs/README.md # Cómo generar certificados .local
```
---
## 2. Requisitos
- **Docker Engine** y **Docker Compose** v2 (`docker compose`)
- El usuario debe pertenecer al grupo `docker`:
```bash
sudo usermod -aG docker "$USER"
newgrp docker # o cerrá y volvé a iniciar sesión
```
- Puertos **80/443** libres en el host
- Para HTTPS público: el dominio debe apuntar por DNS a la IP del servidor
---
## 3. Puesta en marcha
### 3.1 Estructura y red compartida
```bash
mkdir -p ~/srv/core ~/srv/projects
docker network create proxy_network # red externa compartida
```
`proxy_network` es **externa**: la crea una vez y la reutilizan el proxy y
todos los proyectos.
### 3.2 Almacenamiento de certificados ACME
```bash
mkdir -p ~/srv/core/data
touch ~/srv/core/data/acme.json
chmod 600 ~/srv/core/data/acme.json
```
> `acme.json` contiene la clave privada de la cuenta de Let's Encrypt.
> **Nunca se versiona** (ya está en `.gitignore`).
### 3.3 Levantar el proxy
```bash
cd ~/srv/core
docker compose up -d
docker ps | grep core-proxy
```
Traefik queda escuchando en `:80` (HTTP) y `:443` (HTTPS).
---
## 4. Cómo funciona el proxy
| Concepto | Detalle |
|---|---|
| **Provider Docker** | Traefik lee `/var/run/docker.sock` (solo lectura) y detecta contenedores con `traefik.enable=true` |
| `exposedByDefault: false` | Solo se publican los contenedores etiquetados |
| **Provider File** | Carga certificados estáticos desde `dynamic_tls.yml` con `watch: true` |
| `proxy_network` | Red externa por la que Traefik alcanza a los servicios |
| Red `default` | Red bridge **aislada** del proxy para bases de datos, colas, etc. |
| `no-new-privileges` | Endurecimiento básico del contenedor del proxy |
| Dashboard | Habilitado en `api.dashboard` (ver [Seguridad](#10-seguridad)) |
### Puertos expuestos por el proxy
```yaml
ports:
- "80:80" # entrypoint: web → HTTP
- "443:443" # entrypoint: websecure → HTTPS
```
---
## 5. Crear un proyecto base
```bash
cd ~/srv
./init_project.sh api-ventas api.midominio.com 80
```
Genera:
```text
~/srv/projects/api-ventas/
├── app/ # Código fuente, repo o Dockerfile
├── data/
│ ├── db/ # Persistencia de base de datos
│ └── storage/ # Archivos del usuario
├── .env # Variables (chmod 600)
└── docker-compose.yml # Conectado a proxy_network
```
El directorio del proyecto queda con permisos `700`.
### Pasos siguientes
```bash
# 1. Colocar el código
git clone https://github.com/tu-usuario/api-ventas.git \
~/srv/projects/api-ventas/app/
# o bien:
cp -r /ruta/a/tus/archivos/* ~/srv/projects/api-ventas/app/
# 2. Ajustar docker-compose.yml
# - Opción A: descomentar build: { context: ./app, dockerfile: Dockerfile }
# - Opción B: cambiar image: api-ventas:latest por tu imagen
# - Servicios extra (Postgres, Redis) → red `default` (aislados del proxy)
# 3. Levantar
cd ~/srv/projects/api-ventas
docker compose up -d
```
### Variables generadas en `.env`
```ini
APP_NAME=api-ventas
APP_DOMAIN=api.midominio.com
PORT=80
```
Agregá ahí tus credenciales. **Nunca** subas ese archivo a Git.
---
## 6. HTTP / HTTPS
Detalle completo en **[`docs/http-https.md`](docs/http-https.md)**.
### A) Dominio público + Let's Encrypt
```yaml
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy_network"
# Middleware de redirección HTTP → HTTPS
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true"
# Router HTTP (redirige)
- "traefik.http.routers.miapp-http.rule=Host(`api.midominio.com`)"
- "traefik.http.routers.miapp-http.entrypoints=web"
- "traefik.http.routers.miapp-http.middlewares=redirect-to-https"
# Router HTTPS (emite el certificado)
- "traefik.http.routers.miapp-https.rule=Host(`api.midominio.com`)"
- "traefik.http.routers.miapp-https.entrypoints=websecure"
- "traefik.http.routers.miapp-https.tls=true"
- "traefik.http.routers.miapp-https.tls.certresolver=letsencrypt"
# Servicio interno
- "traefik.http.services.miapp.loadbalancer.server.port=80"
```
### B) Dominio local `.local` + certificado auto-firmado
1. Generar el certificado:
```bash
cd ~/srv/core/data/certs
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout miapp.local.key -out miapp.local.crt \
-subj "/CN=miapp.local" -addext "subjectAltName=DNS:miapp.local"
```
2. Declararlo en `core/data/dynamic_tls.yml`:
```yaml
tls:
certificates:
- certFile: /etc/traefik/certs/miapp.local.crt
keyFile: /etc/traefik/certs/miapp.local.key
```
3. Etiquetar el proyecto **sin** certresolver:
```yaml
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy_network"
- "traefik.http.routers.miapp.rule=Host(`miapp.local`)"
- "traefik.http.routers.miapp.entrypoints=websecure"
- "traefik.http.routers.miapp.tls=true"
- "traefik.http.services.miapp.loadbalancer.server.port=80"
```
4. Reiniciar el proxy y resolver el nombre en el cliente:
```bash
cd ~/srv/core && docker compose restart traefik
echo "192.168.1.50 miapp.local" | sudo tee -a /etc/hosts
```
Instrucciones completas (SAN múltiple, instalación del CA en cada SO) en
**[`core/data/certs/README.md`](core/data/certs/README.md)**.
### C) Sin redirección (solo HTTP)
Para entornos internos donde no interese HTTPS:
```yaml
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy_network"
- "traefik.http.routers.miapp.rule=Host(`miapp.local`)"
- "traefik.http.routers.miapp.entrypoints=web"
- "traefik.http.services.miapp.loadbalancer.server.port=80"
```
---
## 7. Comandos útiles
```bash
# Estado y logs del proxy
docker ps | grep core-proxy
docker logs -f core-proxy
# Recargar tras editar dynamic_tls.yml
cd ~/srv/core && docker compose restart traefik
# Proyecto: ciclo de vida
cd ~/srv/projects/miapp
docker compose up -d # levantar
docker compose up -d --build # reconstruir imagen
docker compose restart # reiniciar
docker compose down # detener (conserva datos)
docker compose logs -f web
# Entrar al contenedor
docker exec -it miapp-web bash
# Ver redes de un contenedor
docker network inspect proxy_network --format '{{.Containers}}'
```
---
## 8. Ejemplo: proyecto Laravel + PostgreSQL
> Ejemplo **anonimizado** con nombres genéricos. Los proyectos reales se
> publican siempre sin nombres, dominios ni credenciales del servidor real:
> ver [`docs/publicacion-anonima.md`](docs/publicacion-anonima.md).
```yaml
services:
web:
build:
context: ./app
dockerfile: Dockerfile
container_name: miapp-web
restart: unless-stopped
env_file: [ .env ]
volumes:
- ./app/src:/var/www/miapp
- ./data/storage:/var/www/miapp/storage
networks: [ default, proxy_network ]
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy_network"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.routers.miapp-http.rule=Host(`miapp.local`)"
- "traefik.http.routers.miapp-http.entrypoints=web"
- "traefik.http.routers.miapp-http.middlewares=redirect-to-https"
- "traefik.http.routers.miapp-https.rule=Host(`miapp.local`)"
- "traefik.http.routers.miapp-https.entrypoints=websecure"
- "traefik.http.routers.miapp-https.tls=true"
- "traefik.http.routers.miapp-https.service=miapp-svc"
- "traefik.http.services.miapp-svc.loadbalancer.server.port=80"
pg:
image: postgres:14
container_name: miapp-pg
networks: [ default ] # ← aislada: no accesible desde el proxy
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes: [ ./data/db:/var/lib/postgresql ]
networks:
default:
driver: bridge
proxy_network:
external: true
```
> ⚠️ Mové las credenciales de `environment:` al `.env` del proyecto.
> Las contraseñas escritas directamente en el `docker-compose.yml` terminan en
> el historial de Git.
---
## 9. Solución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
| `404 Not Found` | Contenedor sin `traefik.enable=true` o fuera de `proxy_network` | `docker network inspect proxy_network` |
| El router no aparece en el dashboard | Servicio sin `traefik.enable` o red incorrecta | Verificar etiquetas y redes del compose |
| `502 Bad Gateway` | El servicio interno no escucha en el puerto declarado | `docker exec -it <cont> sh -c 'netstat -tlnp'` |
| Certificado no se emite (Let's Encrypt) | DNS no apunta al server, o 80/443 bloqueados | `dig +short dominio`, revisar firewall |
| `too many certificates` / `rate limit` | Se emitted de más en staging/producción | Revisar `acme.json` y esperar la ventana de rate limit |
| `port is already allocated` | Otro servicio ocupa 80/443 | `docker ps --filter publish=80` |
| `permission denied` en Docker socket | Usuario fuera del grupo `docker` | `sudo usermod -aG docker $USER` y re-loguear |
| El `.crt` local no se aplica | Ruta incorrecta en `dynamic_tls.yml` | Usar la ruta **dentro del contenedor** (`/etc/traefik/certs/...`) y reiniciar |
| `no such file or directory: /acme.json` | Falta el archivo o permisos | `touch acme.json && chmod 600 acme.json` |
| Cambios en `traefik.yml` sin efecto | El archivo está montado `:ro` | `docker compose restart traefik` |
Diagnóstico rápido:
```bash
docker logs -f core-proxy | grep -iE "error|warn"
curl -sI http://miapp.local # debería devolver 301 → https
curl -sk https://miapp.local -I # -k ignora el certificado auto-firmado
```
---
## 10. Seguridad
- **Nunca subas**: `.env`, `acme.json`, `*.key`, `*.crt`, contraseñas de bases de
datos, dumps SQL ni respaldos. El `.gitignore` de este repo lo cubre.
- Los `.env` por proyecto se crean con `chmod 600`.
- `docker.sock` se monta **solo lectura** (`ro`) en Traefik.
- El dashboard está habilitado. En producción:
- agregá `basicAuth` / `forwardAuth` a las etiquetas del dashboard, o
- deshabilitalo con `api.dashboard: false`.
- Traefik corre con `no-new-privileges:true`.
- `acme.json` debe quedar en `600` o el proxy no managea los certificados.
- Restringí los puertos publicados de las bases de datos (`ports:`) a `127.0.0.1`
si solo los necesitás desde el host:
```yaml
ports: ["127.0.0.1:5432:5432"]
```
---
## 11. Publicación anónima
Todo lo que se sube a un repositorio del laboratorio se publica **anonimizado**.
Este repositorio es el ejemplo canónico: no contiene nombres de personas, dominios
reales, direcciones IP, nombres de proyectos internos ni credenciales.
**Reglas:**
1. Nombres de proyecto → genéricos (`miapp`, `ejemplo`, `demo`).
2. Dominios → `*.local`, `example.com`, `midominio.com`. Nunca los reales.
3. Rutas y usuarios → `~/srv`, `$USER`. Nunca `/home/<usuario-real>/`.
4. Credenciales → placeholders `${DB_PASSWORD}` y `.env.example`.
5. IPs → RFC 5737 (`192.0.2.0/24`, `198.51.100.0/24`) o `192.168.0.0/16`.
6. Nada de `.env`, `acme.json`, certificados, bases de datos ni respaldos.
7. Metadatos de commit: revisar `git log` antes de publicar.
Procedimiento completo y checklist en
**[`docs/publicacion-anonima.md`](docs/publicacion-anonima.md)**.
Verificación automática:
```bash
./tools/verificar-anonimato.sh
```
---
## 12. Actualizar el proxy
Este repositorio es la **capa de infraestructura**. Cada proyecto se mantiene en
su propio repositorio y se despliega en `~/srv/projects/<proyecto>/` siguiendo
los pasos de este README.
```bash
cd ~/srv/core && docker compose pull && docker compose up -d # actualizar proxy
```
---
## Licencia
**GPL-3.0** (GNU General Public License, versión 3) — texto completo en
[`LICENSE`](LICENSE).
Software libre bajo los principios del movimiento de Software Libre: podés
usar, estudiar, copiar, modificar y redistribuir este software. Si lo
distribuís o lo desplegás para terceros, debés publicar también las fuentes
de tus modificaciones bajo la misma licencia.
Autoría: Laboratorio de Investigación en Software Libre.
### Racional de la licencia
**GPL-3.0 es una decisión deliberada, no una escolha por omisión.** No debe
cambiarse sin revisar este apartado.
El criterio del laboratorio es **proteger la libertad sin atar a nadie**:
- **No impone condiciones de uso.** Quien instala el proxy, lo corre en su
servidor o lo modifica para uso interno no debe nada a nadie.
- **La obligación aparece solo al distribuir.** Si entregás el código a
terceros —incluido un servicio comercial—, debés publicar los cambios bajo la
misma licencia. Nadie puede cerrar el proyecto sobre trabajo derivado de acá.
- **Se descartó la AGPL-3.0** (`AGPL-3.0-or-later`) porque su cláusula 13 extiende
ese deber también al **uso en red**: obligaría a publicar los cambios
incluso en un servicio interno no distribuido. El laboratorio lo considera
más restrictivo de lo necesario y contrario al objetivo de no imponer
condiciones más allá de la distribución.
En resumen: *copyleft estricto en la distribución, libertad total en el uso*.
Si en el futuro se evaluara adoptar una licencia más permisiva (MIT, BSD,
Apache-2.0), debe resolverse en el mismo sentido: consultando a quienes
participan y revisando cómo afecta a la distribución de versiones anteriores.