fix(security): isolate agent authentication state

This commit is contained in:
2026-08-11 09:55:17 +02:00
parent 5ff1165a2d
commit e77aac8d94
6 changed files with 17 additions and 13 deletions
+2 -2
View File
@@ -48,7 +48,7 @@ docker compose pull
docker compose up -d
```
Never delete `data`, the server/backup directories or the internal `agent_state` volume during an update.
Never delete `data`, the server/backup directories or the internal `agent_state` and `agent_auth` volumes during an update.
## Volumes and persistent data
@@ -58,7 +58,7 @@ Never delete `data`, the server/backup directories or the internal `agent_state`
| `DOGAMA_SERVERS_PATH` | `/srv/game-servers` | Game-server configuration and player data |
| `DOGAMA_BACKUPS_PATH` | `/srv/game-backups` | DoGaMa-managed game backups |
`agent_state` is an internal named volume. It contains the shared authentication token and the authenticated agent registry that prevents operations against unknown containers. It is not a user configuration surface, but it must be backed up with the other DoGaMa state.
`agent_state` and `agent_auth` are internal named volumes. The first contains the authenticated agent registry that prevents operations against unknown containers; the second contains only the shared authentication token and is the only agent storage mounted read-only by the application. They are not user configuration surfaces, but both must be backed up with the other DoGaMa state.
## Ports
+3 -1
View File
@@ -10,7 +10,7 @@ services:
DOGAMA_AGENT_URL: http://agent:8081
volumes:
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent:ro
- agent_auth:/var/lib/dogama-agent/secrets:ro
- ${DOGAMA_SERVERS_PATH:-./servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./backups}:/srv/game-backups
tmpfs:
@@ -36,6 +36,7 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- agent_state:/var/lib/dogama-agent
- agent_auth:/var/lib/dogama-agent/secrets
- ${DOGAMA_SERVERS_PATH:-./servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./backups}:/srv/game-backups
tmpfs:
@@ -55,3 +56,4 @@ networks:
volumes:
agent_state:
agent_auth:
+2 -2
View File
@@ -38,7 +38,7 @@ Read this compact operational baseline before starting a milestone. Open detaile
- Linux black-box bootstrap/authentication E2E coverage plus a documented disposable-Docker V1 release verification matrix.
- Deterministic Linux `amd64`/`arm64` archives with embedded build identity, SPDX module SBOM and SHA-256 checksums.
- Minimal production Compose containing only `dogama` and `agent`; `docker compose up -d` is the complete first-start workflow with no initializer, setup command, host UID/GID preparation or user-managed internal secrets.
- Service-owned persistent secrets: the agent atomically creates and validates its mode-`0640` shared token in `agent_state`; the application independently creates and validates its mode-`0600` master key below the application data path. The application tolerates concurrent first start by waiting up to 60 seconds for the token and authenticated agent health.
- Service-owned persistent secrets: the agent atomically creates and validates its mode-`0640` shared token in the internal `agent_auth` volume; the application mounts only that secret directory read-only and independently creates and validates its mode-`0600` master key below the application data path. The application tolerates concurrent first start by waiting up to 60 seconds for the token and authenticated agent health.
- Two service networks: an administrator-named application/reverse-proxy network plus a private Compose control network. The agent safely ensures the fixed `DOGAMA_GAMES_NETWORK` exists and applies it to every game-container create or replacement; it is not caller-selectable through the lifecycle API.
- Portable fresh bind-mount startup uses root identities inside the read-only, capability-free container namespaces and a private process umask. No recursive ownership change is performed; game-container UID/GID remains per-instance configuration.
- Gitea CI for pull requests and `main`, plus tag-only multi-architecture image publication and Gitea Release creation.
@@ -60,7 +60,7 @@ Read this compact operational baseline before starting a milestone. Open detaile
- Update candidates are explicit `tag@sha256:digest` references. Mutable tags alone are rejected; automatic updates remain disabled.
- Mod configuration is data-only. Provider commands, scripts and arbitrary download URLs are forbidden.
- The application generates and retains its 32-byte master key outside SQLite; the agent never receives it. Ciphertext is authenticated AES-GCM and secrets are never returned by list APIs.
- `agent_state` remains durable because it stores both the shared token and the MAC-protected instance-to-container registry; losing it must never trigger automatic adoption.
- `agent_state` and `agent_auth` remain durable security state: the former stores the MAC-protected instance-to-container registry and the latter stores its shared authentication key. Losing either must never trigger automatic adoption.
- The canonical public deployment has exactly two services. Setup sidecars, init containers, host bootstrap scripts and user-managed internal secrets require an explicit architectural decision.
- `compose.yaml` is the sole canonical Compose definition; documentation references it instead of duplicating it.
- `DOGAMA_NETWORK` names the application-facing network. `DOGAMA_GAMES_NETWORK` names the single agent-approved network for all created and recreated game containers; API input cannot override either bootstrap boundary.
+1 -1
View File
@@ -83,7 +83,7 @@ V1 templates do not expose arbitrary Docker security options. The agent applies
## Authentication and replay defense
At its first start, the agent generates the shared token automatically in the internal `agent_state` volume with mode `0640`. It validates and reuses an existing token and refuses invalid content or permissions without replacement. The application receives the volume read-only, waits for the token and authenticated agent health, and the agent never receives the application master key. Sign method, path, body digest, timestamp and nonce. Reject clock-skewed or reused nonces. Use constant-time comparison, small body limits and short timeouts. Rotate the token through an explicit maintenance workflow.
At its first start, the agent generates the shared token automatically in the internal `agent_auth` volume with mode `0640`. It validates and reuses an existing token and refuses invalid content or permissions without replacement. The application receives only that secret directory read-only, never the registry in `agent_state`, waits for the token and authenticated agent health, and the agent never receives the application master key. Sign method, path, body digest, timestamp and nonce. Reject clock-skewed or reused nonces. Use constant-time comparison, small body limits and short timeouts. Rotate the token through an explicit maintenance workflow.
The wire format is `DoGaMa-HMAC-SHA256 <base64url-signature>` in the
`Authorization` header, with `X-DoGaMa-Timestamp` in RFC 3339 and a random
+6 -6
View File
@@ -11,13 +11,13 @@ docker compose pull
docker compose up -d
```
The deployment has exactly two services: `dogama` and `agent`. On first start, the agent creates its shared token in `agent_state` and the application creates its master encryption key below `DOGAMA_DATA_PATH`. Both use the operating system cryptographic random source, atomic create-without-replacement behavior and restrictive modes. Existing files are validated and reused; an invalid file stops the owning service and is never silently replaced. Secret values never enter `.env`, Compose values, logs, APIs or the UI.
The deployment has exactly two services: `dogama` and `agent`. On first start, the agent creates its shared token in the internal `agent_auth` volume and the application creates its master encryption key below `DOGAMA_DATA_PATH`. Both use the operating system cryptographic random source, atomic create-without-replacement behavior and restrictive modes. Existing files are validated and reused; an invalid file stops the owning service and is never silently replaced. Secret values never enter `.env`, Compose values, logs, APIs or the UI.
The services may start in either order. The application waits up to 60 seconds for the token and an authenticated agent health response. Compose restart policy provides a clean subsequent attempt if the agent or Docker daemon takes longer. An interrupted atomic write leaves no installed partial secret; the next start retries creation. No initializer, setup command or host permission repair is part of this workflow.
The application data path contains SQLite, import staging and the application-only master key. The internal `agent_state` volume contains the agent token and authenticated container-binding registry. The registry is durable security state: losing it makes existing containers unknown rather than silently adopting them. The agent never receives the master key.
The application data path contains SQLite, import staging and the application-only master key. The internal `agent_state` volume contains the authenticated container-binding registry; the separate internal `agent_auth` volume contains only the shared token and is mounted read-only at the application's token directory. Both volumes are durable security state: losing either makes existing containers unknown rather than silently adopting them. The application cannot read the registry and the agent never receives the master key.
Only host-side storage locations, image version, web port, timezone and the two Docker network names are public Compose settings. `DOGAMA_NETWORK` names the application-facing network used by a reverse proxy. `DOGAMA_GAMES_NETWORK` names the sole network that the restricted agent attaches to created and recreated game containers. API plans contain no caller-selectable network. Container paths and allowed agent roots remain fixed internal contracts. Back up the application data, game servers, backups and `agent_state` volume together.
Only host-side storage locations, image version, web port, timezone and the two Docker network names are public Compose settings. `DOGAMA_NETWORK` names the application-facing network used by a reverse proxy. `DOGAMA_GAMES_NETWORK` names the sole network that the restricted agent attaches to created and recreated game containers. API plans contain no caller-selectable network. Container paths and allowed agent roots remain fixed internal contracts. Back up the application data, game servers, backups, `agent_state` and `agent_auth` together.
Both services use root inside their container namespaces so Docker-created bind directories and ordinary administrator-selected paths work without knowledge of an image-specific UID/GID. They keep read-only root filesystems, `no-new-privileges`, an empty Linux capability set and a `0077` process umask for new files. The main application never receives the Docker socket. DoGaMa creates only directories it needs below the configured roots and never performs an automatic recursive `chown` of application, server or backup data. Game-container UID/GID selection remains a separate per-instance setting.
@@ -25,9 +25,9 @@ For NAS or server-style paths, set ordinary writable locations in `.env`, for ex
## Upgrade and rollback
The two-service deployment validates and reuses secrets created by its normal first-start contract. The v0.1.0 initializer used a different application-image identity, so do not claim an unverified in-place migration for a v0.1.0 deployment where initialization completed; preserve all four stores and validate that case on a copy before changing production. For normal updates after this correction, stop DoGaMa, take a filesystem-consistent backup of all four persistent stores, replace the canonical `compose.yaml`, change only `DOGAMA_VERSION`, then run `docker compose pull` and `docker compose up -d`.
The two-service deployment validates and reuses secrets created by its normal first-start contract. The v0.1.0 initializer used a different application-image identity, so do not claim an unverified in-place migration for a v0.1.0 deployment where initialization completed; preserve all five stores and validate that case on a copy before changing production. For normal updates after this correction, stop DoGaMa, take a filesystem-consistent backup of all five persistent stores, replace the canonical `compose.yaml`, change only `DOGAMA_VERSION`, then run `docker compose pull` and `docker compose up -d`.
Startup applies append-only SQLite migrations before serving requests. Never remove or recreate `agent_state`, and never replace an existing master key: doing so would invalidate the agent registry or make encrypted notification data unreadable. On failure, restore all state from the same backup point and select the prior image version.
Startup applies append-only SQLite migrations before serving requests. Never remove or recreate `agent_state` or `agent_auth`, and never replace an existing master key: doing so would invalidate the agent registry or make encrypted notification data unreadable. On failure, restore all state from the same backup point and select the prior image version.
## Release pipeline
@@ -54,4 +54,4 @@ make release VERSION=v0.1.0
make images VERSION=v0.1.0
```
On an isolated Docker host, start with empty application paths and no `agent_state`, then run only `docker compose up -d`. Confirm automatic secret initialization and reuse, authenticated agent health, application bootstrap, Palworld draft/install, lifecycle operations, backup/restore, failed-update rollback, configured game-network attachment and denial against unrelated containers or caller-selected networks. Confirm that only the web port is published, the main application lacks the Docker socket, the agent cannot access the master key, and player/backup data survive `docker compose down` followed by `docker compose up -d` without `-v`.
On an isolated Docker host, start with empty application paths and no `agent_state` or `agent_auth`, then run only `docker compose up -d`. Confirm automatic secret initialization and reuse, authenticated agent health, application bootstrap, Palworld draft/install, lifecycle operations, backup/restore, failed-update rollback, configured game-network attachment and denial against unrelated containers or caller-selected networks. Confirm that only the web port is published, the main application lacks the Docker socket and agent registry, the agent cannot access the master key, and player/backup data survive `docker compose down` followed by `docker compose up -d` without `-v`.
+3 -1
View File
@@ -132,7 +132,9 @@ def validate_compose() -> None:
assert "/var/run/docker.sock:/var/run/docker.sock" not in services["dogama"].get("volumes", []), "main application mounts Docker"
assert not services["agent"].get("ports"), "agent must not publish a port"
assert "agent_state:/var/lib/dogama-agent" in services["agent"]["volumes"], "authenticated agent registry is not persistent"
assert "agent_state:/var/lib/dogama-agent:ro" in services["dogama"]["volumes"], "main application cannot read the shared token"
assert "agent_auth:/var/lib/dogama-agent/secrets:ro" in services["dogama"]["volumes"], "main application cannot read the shared token"
assert "agent_state:/var/lib/dogama-agent:ro" not in services["dogama"]["volumes"], "main application must not read the agent registry"
assert "agent_auth:/var/lib/dogama-agent/secrets" in services["agent"]["volumes"], "agent token is not persistent"
assert all("master_key" not in volume for volume in services["agent"]["volumes"]), "agent can access the master key"
rendered = (ROOT / "compose.yaml").read_text(encoding="utf-8")
assert "${DOGAMA_SERVERS_PATH:-./servers}:/srv/game-servers" in rendered