SA-007 trivial docker compose

This commit is contained in:
2026-08-26 18:20:04 +02:00
parent 4049a197b9
commit c3c3c8e1a6
9 changed files with 72 additions and 19 deletions
+3 -2
View File
@@ -11,8 +11,9 @@ LINKLOG_APP_NAME=LinkLog
LINKLOG_VERSION=0.1.0 LINKLOG_VERSION=0.1.0
LINKLOG_SECRET_KEY=replace-with-a-long-random-secret LINKLOG_SECRET_KEY=replace-with-a-long-random-secret
LINKLOG_DATA_ENCRYPTION_KEY=generate-with-python-cryptography-fernet-key LINKLOG_DATA_ENCRYPTION_KEY=generate-with-python-cryptography-fernet-key
LINKLOG_TOKEN_EXPIRY_DAYS=30 LINKLOG_TOKEN_EXPIRY_MINUTES=15
LINKLOG_PUBLIC_URL=localhost LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS=30
LINKLOG_PUBLIC_URL=linklog.example.com
LINKLOG_SMTP_HOST= LINKLOG_SMTP_HOST=
LINKLOG_SMTP_PORT=587 LINKLOG_SMTP_PORT=587
LINKLOG_SMTP_USERNAME= LINKLOG_SMTP_USERNAME=
+17 -3
View File
@@ -12,7 +12,8 @@ frontend/ Jinja templates and browser-side assets
webextension/ Firefox Manifest V3 extension webextension/ Firefox Manifest V3 extension
Logo.svg Source logo artwork used by the web and extension interfaces Logo.svg Source logo artwork used by the web and extension interfaces
Dockerfile Backend container image 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 REQUIREMENTS.md Product requirements
VIBE/ Conversation and prompt logs 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. 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
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`. 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_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_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_DATABASE_PATH` | SQLite file path inside the container | `/app/backend/data/linklog.db` |
| `LINKLOG_TOKEN_EXPIRY_DAYS` | access-token lifetime | `30` | | `LINKLOG_TOKEN_EXPIRY_MINUTES` | access-token lifetime | `15` |
| `LINKLOG_PUBLIC_URL` | Public hostname used by Traefik and expanded to a callback URL by the backend | `localhost` | | `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_HOST` | SMTP server hostname; empty disables delivery in local development | empty |
| `LINKLOG_SMTP_PORT` | SMTP server port | `587` | | `LINKLOG_SMTP_PORT` | SMTP server port | `587` |
| `LINKLOG_SMTP_USERNAME` | SMTP login username | empty | | `LINKLOG_SMTP_USERNAME` | SMTP login username | empty |
+9 -5
View File
@@ -113,13 +113,17 @@ These findings are prioritized below. Severity describes the potential security
### SA-007: Production Compose configuration exposes the application directly ### SA-007: Production Compose configuration exposes the application directly
**Severity:** Medium/High **Severity:** Medium/High, remediated in current worktree
**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. **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. **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 ### 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. - [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. - [ ] 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. - [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. - [ ] Configure HTTPS, HSTS, CSP, Referrer-Policy, frame protections, `nosniff`, and trusted hosts.
- [ ] Define a restrictive CORS policy or leave CORS disabled. - [ ] Define a restrictive CORS policy or leave CORS disabled.
- [ ] Add global request-size limits and hardened image decoding/re-encoding. - [ ] Add global request-size limits and hardened image decoding/re-encoding.
+6
View File
@@ -1,5 +1,11 @@
# Chat Log # 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 ### 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. 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.
+1
View File
@@ -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. 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. 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. 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 ## Future entries
+1 -1
View File
@@ -27,7 +27,7 @@ class Settings:
data_encryption_key: str = os.getenv('LINKLOG_DATA_ENCRYPTION_KEY', '') data_encryption_key: str = os.getenv('LINKLOG_DATA_ENCRYPTION_KEY', '')
token_expiry_minutes: int = int(os.getenv('LINKLOG_TOKEN_EXPIRY_MINUTES', '15')) 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')) 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_host: str = os.getenv('LINKLOG_SMTP_HOST', '')
smtp_port: int = int(os.getenv('LINKLOG_SMTP_PORT', '587')) smtp_port: int = int(os.getenv('LINKLOG_SMTP_PORT', '587'))
smtp_username: str = os.getenv('LINKLOG_SMTP_USERNAME', '') smtp_username: str = os.getenv('LINKLOG_SMTP_USERNAME', '')
+2 -3
View File
@@ -4,8 +4,6 @@ services:
app: app:
image: git.kolkman.org/olaf/link-log:development # or :latest or a version-tag image: git.kolkman.org/olaf/link-log:development # or :latest or a version-tag
container_name: ${APP_CONTAINER_NAME:-linklog-app} container_name: ${APP_CONTAINER_NAME:-linklog-app}
ports:
- "${APP_PORT:-8000}:8000"
volumes: volumes:
- ./linklog_data:/app/backend/data - ./linklog_data:/app/backend/data
environment: environment:
@@ -13,7 +11,8 @@ services:
LINKLOG_APP_NAME: ${LINKLOG_APP_NAME:-LinkLog} LINKLOG_APP_NAME: ${LINKLOG_APP_NAME:-LinkLog}
LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db}
LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} 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_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-linklog.example.com}
LINKLOG_SMTP_HOST: ${LINKLOG_SMTP_HOST:-smtp.example.com} LINKLOG_SMTP_HOST: ${LINKLOG_SMTP_HOST:-smtp.example.com}
LINKLOG_SMTP_PORT: ${LINKLOG_SMTP_PORT:-587} LINKLOG_SMTP_PORT: ${LINKLOG_SMTP_PORT:-587}
+30
View File
@@ -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}
+3 -5
View File
@@ -7,8 +7,6 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: ${APP_CONTAINER_NAME:-linklog-app} container_name: ${APP_CONTAINER_NAME:-linklog-app}
ports:
- "${APP_PORT:-8000}:8000"
volumes: volumes:
- ./linklog_data:/app/backend/data - ./linklog_data:/app/backend/data
environment: environment:
@@ -17,7 +15,7 @@ services:
LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db}
LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} 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_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_LOG_LEVEL: ${LINKLOG_LOG_LEVEL:-INFO}
LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30} LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30}
LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-} LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-}
@@ -36,10 +34,10 @@ services:
traefik.http.routers.linklog.entrypoints: web 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.middlewares: web-https-redirect,servicests
traefik.http.routers.linklog-secure.entrypoints: websecure 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.tls: true
traefik.http.routers.linklog-secure.middlewares: servicests traefik.http.routers.linklog-secure.middlewares: servicests