fix(security): isolate agent authentication state
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user