Skip to content

fix: [TESIS-130] harden the production config: secrets, CORS, HTTPS, hosts and seeds - #95

Open
LoLoo03 wants to merge 6 commits into
masterfrom
TESIS-130-security-and-deploy-config
Open

LoLoo03 wants to merge 6 commits into
masterfrom
TESIS-130-security-and-deploy-config

Conversation

@LoLoo03

@LoLoo03 LoLoo03 commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Ticket de Jira

https://proyectofinalfrlp.atlassian.net/browse/TESIS-130


Descripción

Cierra la API desplegada, que usaba defaults pensados para desarrollo. CORS respondía * a cualquier origen. Sin DEVISE_JWT_SECRET_KEY, los JWT se firmaban con secret_key_base. force_ssl y assume_ssl estaban comentados, así que no había HSTS y la cookie del backoffice salía sin Secure. Sin RAILS_ALLOWED_HOSTS, el Host no se validaba. Y el db:prepare del entrypoint sembraba cada base nueva con admin123 y password123. Ahora producción no arranca sin sus secretos y hosts: preferimos un deploy que falla a la vista a uno que levanta abierto. Los seeds toman las contraseñas del entorno y rotan las del repo en una base ya sembrada. El detalle y las alternativas descartadas están en ADR-018; la parte del front va en el PR de proyecto-web de la misma card.

  • Restringe CORS (config/initializers/cors.rb) a CORS_ALLOWED_ORIGINS, con comodín de un nivel para el subdominio de cada empresa (https://*.dominio vale para norte.dominio, no para a.b.dominio). Sin la variable, fuera de producción, acepta localhost en cualquier puerto. Sigue exponiendo el ETag
  • Corta el arranque en producción (config/environments/production.rb) si faltan DEVISE_JWT_SECRET_KEY, CORS_ALLOWED_ORIGINS o RAILS_ALLOWED_HOSTS. SECRET_KEY_BASE_DUMMY exime al assets:precompile del Dockerfile
  • Activa assume_ssl y force_ssl: HSTS y cookies Secure, entre ellas _proyecto_api_session. El TLS y la redirección HTTP→HTTPS siguen en Caddy
  • Hace obligatoria la validación del Host y deja /up fuera de ella, para los health checks
  • Cambia db/seeds.rb: en producción las contraseñas salen de SEED_USER_PASSWORD y SEED_ADMIN_PASSWORD, el seed se niega a correr sin ellas o con una del repo, y rota las cuentas que una siembra anterior dejó con las contraseñas del repo
  • Agrega spec/requests/cors_spec.rb y spec/db/seeds_spec.rb
  • Agrega ADR-018, cierra en ADR-017 el ⚠️ de la cookie sin Secure y documenta las variables en .env.example y architecture.md

Evidencia visual

N/A


Cómo probar

  1. Specs: bundle exec rspec spec/requests/cors_spec.rb spec/db/seeds_spec.rb
    → 10 ejemplos en verde. La suite completa da 1556 ejemplos sin fallos.
  2. Arranque sin variables: construir la imagen (docker build -t proyecto_api ., desde Linux o WSL: desde un checkout de Windows falla por los finales CRLF de bin/, que va en una tarea aparte) y correr docker run --rm -e SECRET_KEY_BASE=x proyecto_api ./bin/rails runner 'puts 1'
    → Falla con Missing environment variables for production: DEVISE_JWT_SECRET_KEY, CORS_ALLOWED_ORIGINS, RAILS_ALLOWED_HOSTS.
  3. Imagen de producción con las variables: levantarla contra un Postgres con DEVISE_JWT_SECRET_KEY, CORS_ALLOWED_ORIGINS=https://*.ejemplo.test, RAILS_ALLOWED_HOSTS=api.ejemplo.test,localhost, SEED_USER_PASSWORD y SEED_ADMIN_PASSWORD. Después:
    • curl -I -H 'Host: otro.test' http://localhost:<puerto>/api/v1/tenant-config?slug=norte → 403. Con Host: api.ejemplo.test → 200. /up con cualquier Host → 200.
    • Las respuestas traen Strict-Transport-Security, y GET /admin/sign_in devuelve la cookie con secure.
    • Preflight OPTIONS /api/v1/auth/login con Origin: https://norte.ejemplo.test → devuelve ese origen. Con https://a.b.ejemplo.test, https://evil.example o null → sin Access-Control-Allow-Origin.
    • Un JWT firmado con secret_key_base en vez de DEVISE_JWT_SECRET_KEY → 401 en GET /api/v1/me.
    • bin/rails runner dentro del contenedor → ninguna cuenta valida password123 ni admin123.
  4. Rotación en una base vieja: en ese contenedor, volver a poner password123 a un usuario y correr bin/rails db:seed
    → Sin las variables SEED_* falla con un mensaje claro. Con ellas, el usuario deja de validar password123.

Impacto y consideraciones

¿Introduce breaking changes?
Sí. En producción la API no arranca sin las tres variables de abajo, y el front desplegado sólo puede llamarla desde los orígenes de CORS_ALLOWED_ORIGINS. Hay que cargarlas en el server antes de deployar. Además, cambiar DEVISE_JWT_SECRET_KEY cierra las sesiones abiertas. En desarrollo no cambia nada: CORS acepta localhost y los seeds siguen con las contraseñas de siempre.

¿Requiere nuevas variables de entorno?
Sí, sólo en producción:

  • DEVISE_JWT_SECRET_KEY: obligatoria, una por entorno (bin/rails secret)
  • CORS_ALLOWED_ORIGINS=https://*.precision-logistics.duckdns.org: obligatoria
  • RAILS_ALLOWED_HOSTS=api.precision-logistics.duckdns.org: obligatoria
  • SEED_USER_PASSWORD y SEED_ADMIN_PASSWORD: las pide db:seed. Correr bin/rails db:seed una vez con ellas en la demo actual rota las cuentas que siguen con las contraseñas del repo

¿Afecta la arquitectura o genera un nuevo patrón?
Sí. Agrega docs/adr/ADR-018-seguridad-del-despliegue.md, que fija variables obligatorias con arranque que falla en producción, CORS por lista con comodín de subdominio, TLS terminado en el proxy con assume_ssl y seeds sin contraseñas del repo. Queda como deuda anotada que ENCRYPTION_KEY todavía cae a secret_key_base: cambiarla vuelve ilegibles las credenciales ya cifradas, así que necesita su propia migración.


🤖 Generated with Claude Code

LorenzoBellomo and others added 5 commits September 29, 2026 11:31
The API answered Access-Control-Allow-Origin: * to any origin, so any page
could call it from its visitors' browsers, for instance spreading login
attempts across many IPs to dodge the per-IP limit. The origins now come
from CORS_ALLOWED_ORIGINS, with a one-level wildcard for the tenant
subdomain; without it, outside production, only localhost is accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… HTTPS only

Production now refuses to boot without DEVISE_JWT_SECRET_KEY,
CORS_ALLOWED_ORIGINS and RAILS_ALLOWED_HOSTS instead of signing tokens with
secret_key_base and skipping the Host check. assume_ssl and force_ssl are
on behind the TLS proxy, which adds HSTS and marks the backoffice session
cookie as Secure. /up stays out of the host check for the health checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Docker entrypoint runs db:prepare, which seeds every new database, so a
deploy started with admin123 and password123. In production the seeded
passwords now come from SEED_USER_PASSWORD and SEED_ADMIN_PASSWORD, the
seed refuses to run without them or with a repo password, and it rotates
the accounts a previous seed left with the repo passwords.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CRLF scripts

With core.autocrlf=true, git checks out bin/* with CRLF, so the shebang of
bin/rails asks env for `ruby\r` and `docker build` dies at
`./bin/rails assets:precompile` (exit 127). The Dockerfile already strips the
CRs, but that step ran after assets:precompile, which was added above it.

- Move "Adjust binfiles" (chmod +x, strip CR) back before the first RUN that
  executes bin/, the order `rails new` generates. Existing Windows clones keep
  CRLF on disk even after .gitattributes changes, so this is what makes them
  build without re-checking out bin/.
- Force LF on bin/* and *.sh in .gitattributes (CRLF for the bin/kamal.cmd
  batch file), so fresh checkouts and containers that bind-mount the repo get
  runnable scripts. The index already stored them as LF: renormalizing changes
  no blobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LoLoo03
LoLoo03 requested a review from a team as a code owner September 29, 2026 17:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant