From c3c3c8e1a67a39e510702194b9620074d59f6fbf Mon Sep 17 00:00:00 2001 From: Kolkman Date: Wed, 26 Aug 2026 18:20:04 +0200 Subject: [PATCH] SA-007 trivial docker compose --- .env.example | 5 +++-- README.md | 20 +++++++++++++++++--- Security-audit.md | 14 +++++++++----- VIBE/CHAT_LOG.md | 6 ++++++ VIBE/PROMPTS.md | 1 + backend/app/core/config.py | 2 +- docker-compose-example.yml | 5 ++--- docker-compose.local.yml | 30 ++++++++++++++++++++++++++++++ docker-compose.yml | 8 +++----- 9 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 docker-compose.local.yml diff --git a/.env.example b/.env.example index 4e32629..540d46c 100644 --- a/.env.example +++ b/.env.example @@ -11,8 +11,9 @@ LINKLOG_APP_NAME=LinkLog LINKLOG_VERSION=0.1.0 LINKLOG_SECRET_KEY=replace-with-a-long-random-secret LINKLOG_DATA_ENCRYPTION_KEY=generate-with-python-cryptography-fernet-key -LINKLOG_TOKEN_EXPIRY_DAYS=30 -LINKLOG_PUBLIC_URL=localhost +LINKLOG_TOKEN_EXPIRY_MINUTES=15 +LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS=30 +LINKLOG_PUBLIC_URL=linklog.example.com LINKLOG_SMTP_HOST= LINKLOG_SMTP_PORT=587 LINKLOG_SMTP_USERNAME= diff --git a/README.md b/README.md index 7c47e96..a87a8a7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ frontend/ Jinja templates and browser-side assets webextension/ Firefox Manifest V3 extension Logo.svg Source logo artwork used by the web and extension interfaces Dockerfile Backend container image -docker-compose.yml App with traefik reverse proxy hooks +docker-compose.yml Production app with Traefik reverse proxy hooks +docker-compose.local.yml Local development app with direct port access REQUIREMENTS.md Product requirements VIBE/ Conversation and prompt logs ``` @@ -94,6 +95,18 @@ make xpi This creates `XPI/unsigned/LinkLog-0.1.0.xpi` from the `webextension/` package and excludes macOS metadata and minified artifacts. The version is read from `webextension/manifest.json`. The `XPI/signed/` directory is reserved for signed release bundles. +## Docker Deployment + +The main `docker-compose.yml` is the production deployment. It does not publish port 8000 on the host; the application is reachable through Traefik on the external `linklog_traefik` network. Set `LINKLOG_PUBLIC_URL` to the DNS hostname served by Traefik. The default is the documentation hostname `linklog.example.com`, which must be replaced for a real deployment. + +For local development with direct access, use the separate file: + +```sh +docker compose -f docker-compose.local.yml up --build +``` + +This publishes `${APP_PORT:-8000}` and defaults the application URL to `http://localhost:8000`. Do not use the local file for an Internet-facing deployment. + ## Releases Releases run in Gitea Actions when a `v*` tag is pushed. The Docker release version comes from `frontend/version.json`; the Firefox plugin version comes from `webextension/manifest.json`. CI also requires both to match `LINKLOG_VERSION`'s default in `backend/app/core/config.py`. @@ -171,8 +184,9 @@ The main configurable values are: | `LINKLOG_SECRET_KEY` | token signing/security secret | required in Docker | | `LINKLOG_DATA_ENCRYPTION_KEY` | Fernet key for encrypting SMTP, Mastodon, and OTP secrets at rest | required in Docker | | `LINKLOG_DATABASE_PATH` | SQLite file path inside the container | `/app/backend/data/linklog.db` | -| `LINKLOG_TOKEN_EXPIRY_DAYS` | access-token lifetime | `30` | -| `LINKLOG_PUBLIC_URL` | Public hostname used by Traefik and expanded to a callback URL by the backend | `localhost` | +| `LINKLOG_TOKEN_EXPIRY_MINUTES` | access-token lifetime | `15` | +| `LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS` | refresh-token lifetime | `30` | +| `LINKLOG_PUBLIC_URL` | Public hostname used by Traefik and expanded to a callback URL by the backend | `linklog.example.com` | | `LINKLOG_SMTP_HOST` | SMTP server hostname; empty disables delivery in local development | empty | | `LINKLOG_SMTP_PORT` | SMTP server port | `587` | | `LINKLOG_SMTP_USERNAME` | SMTP login username | empty | diff --git a/Security-audit.md b/Security-audit.md index 1687164..05bc593 100644 --- a/Security-audit.md +++ b/Security-audit.md @@ -113,13 +113,17 @@ These findings are prioritized below. Severity describes the potential security ### SA-007: Production Compose configuration exposes the application directly -**Severity:** Medium/High -**Evidence:** `docker-compose.yml` publishes `${APP_PORT:-8000}:8000` while also configuring Traefik labels. The file uses an external `linklog_traefik` network and deployment-specific labels. +**Severity:** Medium/High, remediated in current worktree +**Evidence before remediation:** `docker-compose.yml` published `${APP_PORT:-8000}:8000` while also configuring Traefik labels. The file uses an external `linklog_traefik` network and deployment-specific labels. **Impact:** The application can bypass the reverse proxy and any TLS, authentication middleware, rate limiting, or security headers configured there. The default `LINKLOG_PUBLIC_URL=localhost` is also unsuitable for a public deployment. A network or label mismatch can silently expose an unprotected direct endpoint or make operators disable controls to restore access. -**Recommendation:** Split local development and production Compose files. In production, do not publish the application port to the host; attach it only to the reverse-proxy network. Ensure the app and Traefik share the same explicitly named network and validate the effective rendered configuration in CI. Require a non-local public hostname, TLS, security headers, request-size limits, and proxy-level rate limiting. Keep the direct port only in a documented local profile. +**Current state:** The production `docker-compose.yml` no longer publishes port 8000 and attaches the app only to the external `linklog_traefik` network. Its public hostname fallback is `linklog.example.com`, and both Traefik routers use the same `LINKLOG_PUBLIC_URL`. Direct host access is available only through the explicitly named `docker-compose.local.yml` development file. `.env.example` and application settings use `linklog.example.com` as the documented default. -**Priority:** High for Internet-facing deployments. +**Residual impact:** Operators must replace the documentation hostname, ensure the external network is the one used by Traefik, and validate the rendered Compose configuration and proxy middleware in their deployment. The local Compose file must not be exposed to the Internet. + +**Recommendation:** Keep production and local Compose invocations separate, require a real DNS hostname and TLS in deployment checks, and add CI validation for the rendered production configuration and network labels. + +**Priority:** Completed in code; deployment validation remains. ### SA-008: Initial setup and SMTP validation are unauthenticated by design @@ -239,7 +243,7 @@ Before production exposure: - [x] Encrypt newly written SMTP, Mastodon, OAuth, and OTP secrets at rest; protect the encryption key separately. Rotate legacy plaintext values. - [ ] Add login, OTP, reset-mail, and setup rate limiting. - [x] Validate Mastodon instances as HTTPS public hostnames, reject unsafe DNS/IP ranges, and block redirects. Keep network-level egress controls in production. -- [ ] Disable direct host publication of the application port in production. +- [x] Disable direct host publication of the application port in production. - [ ] Configure HTTPS, HSTS, CSP, Referrer-Policy, frame protections, `nosniff`, and trusted hosts. - [ ] Define a restrictive CORS policy or leave CORS disabled. - [ ] Add global request-size limits and hardened image decoding/re-encoding. diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index e061002..b97c630 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -1,5 +1,11 @@ # Chat Log +### User +Address SA-007 and use linklog.example.com as default LINKLOG_PUBLIC_URL. + +### Assistant outcome +Hardened the production Compose deployment by removing direct host port publication, keeping the app on the external Traefik network, and using `linklog.example.com` as the default public hostname. Added `docker-compose.local.yml` for explicit local direct-port development, updated `.env.example`, backend defaults, README deployment instructions, and SA-007 status. + ### User Continue SA-06: Store session credentials in the narrowest available extension storage, minimize token lifetime, support refresh-token rotation, and clear all session material on logout or token invalidation. diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index ac7489b..99b93ef 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -184,6 +184,7 @@ 180. Implement SA-006: remove broad Firefox extension host access and unnecessary tabs permission, request exact configured backend origin access, and keep page capture behind activeTab. 181. Continue to document every prompt and chat in the VIBE directory. 182. Continue SA-006: store session credentials in the narrowest available extension storage, minimize token lifetime, support refresh-token rotation, and clear all session material on logout or token invalidation. +183. Address SA-007 and use linklog.example.com as the default LINKLOG_PUBLIC_URL. ## Future entries diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 39de5b0..9aeb6d3 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -27,7 +27,7 @@ class Settings: data_encryption_key: str = os.getenv('LINKLOG_DATA_ENCRYPTION_KEY', '') token_expiry_minutes: int = int(os.getenv('LINKLOG_TOKEN_EXPIRY_MINUTES', '15')) refresh_token_expiry_days: int = int(os.getenv('LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS', '30')) - public_url: str = normalize_public_url(os.getenv('LINKLOG_PUBLIC_URL', 'http://localhost:8000')) + public_url: str = normalize_public_url(os.getenv('LINKLOG_PUBLIC_URL', 'linklog.example.com')) smtp_host: str = os.getenv('LINKLOG_SMTP_HOST', '') smtp_port: int = int(os.getenv('LINKLOG_SMTP_PORT', '587')) smtp_username: str = os.getenv('LINKLOG_SMTP_USERNAME', '') diff --git a/docker-compose-example.yml b/docker-compose-example.yml index 1da77c9..5fb52e6 100644 --- a/docker-compose-example.yml +++ b/docker-compose-example.yml @@ -4,8 +4,6 @@ services: app: image: git.kolkman.org/olaf/link-log:development # or :latest or a version-tag container_name: ${APP_CONTAINER_NAME:-linklog-app} - ports: - - "${APP_PORT:-8000}:8000" volumes: - ./linklog_data:/app/backend/data environment: @@ -13,7 +11,8 @@ services: LINKLOG_APP_NAME: ${LINKLOG_APP_NAME:-LinkLog} LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} - LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30} + LINKLOG_TOKEN_EXPIRY_MINUTES: ${LINKLOG_TOKEN_EXPIRY_MINUTES:-15} + LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS: ${LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS:-30} LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-linklog.example.com} LINKLOG_SMTP_HOST: ${LINKLOG_SMTP_HOST:-smtp.example.com} LINKLOG_SMTP_PORT: ${LINKLOG_SMTP_PORT:-587} diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..42c2f30 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,30 @@ +# Local development only. The production compose file intentionally does not publish port 8000. + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: ${APP_CONTAINER_NAME:-linklog-app-local} + ports: + - "${APP_PORT:-8000}:8000" + volumes: + - ./linklog_data:/app/backend/data + environment: + APP_ENV: ${APP_ENV:-development} + LINKLOG_APP_NAME: ${LINKLOG_APP_NAME:-LinkLog} + LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} + LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} + LINKLOG_DATA_ENCRYPTION_KEY: ${LINKLOG_DATA_ENCRYPTION_KEY:?Set LINKLOG_DATA_ENCRYPTION_KEY in .env} + LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-http://localhost:8000} + LINKLOG_TOKEN_EXPIRY_MINUTES: ${LINKLOG_TOKEN_EXPIRY_MINUTES:-15} + LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS: ${LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS:-30} + LINKLOG_LOG_LEVEL: ${LINKLOG_LOG_LEVEL:-DEBUG} + LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-} + restart: ${APP_RESTART_POLICY:-unless-stopped} + healthcheck: + test: ["CMD", "python", "-c", "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/health', timeout=3)"] + interval: ${APP_HEALTHCHECK_INTERVAL:-30s} + timeout: ${APP_HEALTHCHECK_TIMEOUT:-5s} + start_period: ${APP_HEALTHCHECK_START_PERIOD:-10s} + retries: ${APP_HEALTHCHECK_RETRIES:-3} diff --git a/docker-compose.yml b/docker-compose.yml index 3fbbabc..c6b7b13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,6 @@ services: context: . dockerfile: Dockerfile container_name: ${APP_CONTAINER_NAME:-linklog-app} - ports: - - "${APP_PORT:-8000}:8000" volumes: - ./linklog_data:/app/backend/data environment: @@ -17,7 +15,7 @@ services: LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} LINKLOG_DATA_ENCRYPTION_KEY: ${LINKLOG_DATA_ENCRYPTION_KEY:?Set LINKLOG_DATA_ENCRYPTION_KEY in .env} - LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-localhost} + LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-linklog.example.com} LINKLOG_LOG_LEVEL: ${LINKLOG_LOG_LEVEL:-INFO} LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30} LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-} @@ -36,10 +34,10 @@ services: traefik.http.routers.linklog.entrypoints: web - traefik.http.routers.linklog.rule: Host(`${LINKLOG_PUBLIC_URL:-localhost}`) + traefik.http.routers.linklog.rule: Host(`${LINKLOG_PUBLIC_URL:-linklog.example.com}`) traefik.http.routers.linklog.middlewares: web-https-redirect,servicests traefik.http.routers.linklog-secure.entrypoints: websecure - traefik.http.routers.linklog-secure.rule: Host(`${LINKLOG_PUBLIC_URL:-localhost}`) + traefik.http.routers.linklog-secure.rule: Host(`${LINKLOG_PUBLIC_URL:-linklog.example.com}`) traefik.http.routers.linklog-secure.tls: true traefik.http.routers.linklog-secure.middlewares: servicests