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.
471 lines
15 KiB
Markdown
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.
|