# TrafficServerLocal > Autoría: **Laboratorio de investigacion en Software Libre Gugler - FCyT**. 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//` 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 │ └── / │ ├── 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 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//`. 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//` 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 investigacion en Software Libre Gugler - FCyT. ### 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.