121 lines
6.0 KiB
Markdown
121 lines
6.0 KiB
Markdown
# Restricted Docker agent
|
|
|
|
## Security objective
|
|
|
|
The agent reduces the chance that an application bug becomes arbitrary Docker control. Because Docker socket access is effectively host-level privilege, the agent is small, independently testable and deny-by-default.
|
|
|
|
## Private API foundation
|
|
|
|
The same-host V1 agent listens on the private control network only. The current
|
|
foundation exposes authenticated health, capacity and typed lifecycle routes:
|
|
|
|
- `GET /v1/health` verifies that the configured Docker Unix socket answers its
|
|
bounded `_ping` request without exposing daemon details;
|
|
- `POST /v1/check-disk` accepts at most 16 existing absolute paths and returns
|
|
capacity only after symlink-aware allowed-root validation;
|
|
- `GET /v1/instances` returns only entries from the authenticated agent-local
|
|
registry;
|
|
- `POST /v1/check-ports` reports only whether requested bindings are available;
|
|
- `POST /v1/instances` creates a validated and registered container;
|
|
- `GET /v1/instances/{id}` and `/stats` inspect only a bound registration;
|
|
- typed `start`, `stop`, `restart` and container `DELETE` routes operate only on
|
|
that registered identity.
|
|
|
|
Every route, including health, requires request authentication. The agent never
|
|
exposes its internal Docker HTTP client as a proxy. Lifecycle requests are
|
|
idempotent where meaningful and re-inspect the container identity and binding
|
|
labels before mutation.
|
|
|
|
## Allowed V1 operations
|
|
|
|
- `CreateInstance(plan)`
|
|
- `StartInstance(instance_id)`
|
|
- `StopInstance(instance_id, timeout)`
|
|
- `RestartInstance(instance_id, timeout)`
|
|
- `ReplaceInstance(plan, expected_revision)` for configuration/update recreation
|
|
- `DeleteContainer(instance_id)` without deleting host data
|
|
- `InspectInstance(instance_id)`
|
|
- `GetInstanceStats(instance_id)`
|
|
- `CheckPorts(bindings)` without disclosing unrelated container details
|
|
- `CheckDisk(paths)`
|
|
- `ListRegisteredInstances()` only
|
|
|
|
There is no generic Docker request, arbitrary command, arbitrary `exec`, arbitrary container ID, image-build endpoint, host filesystem browser or list-all-containers endpoint.
|
|
|
|
## Registration binding
|
|
|
|
Each managed container has labels such as:
|
|
|
|
```text
|
|
io.dogama.managed=true
|
|
io.dogama.instance-id=<opaque-id>
|
|
io.dogama.template-id=<template-id>
|
|
io.dogama.template-version=<version>
|
|
io.dogama.plan-digest=<sha256-of-canonical-plan>
|
|
```
|
|
|
|
Labels alone are insufficient. The agent keeps a durable registry of instance ID, expected container identity and plan digest, authenticated by an agent-local key or MAC. An operation succeeds only when request, registry and inspected labels agree.
|
|
|
|
## Deployment-plan validation
|
|
|
|
Before create or replace, the agent verifies:
|
|
|
|
- instance and template identifiers have valid syntax;
|
|
- image reference matches the canonical approved plan and preferably a resolved digest;
|
|
- command, entrypoint, capabilities, devices and security options exactly match allowed template fields;
|
|
- no privileged mode, host PID/IPC/network namespace, device mount or Docker socket mount;
|
|
- every bind source resolves below an allowed root after symlink-aware canonicalization;
|
|
- every container destination is declared by the template;
|
|
- port protocols and container ports match the template and host ports do not conflict;
|
|
- resource limits are present and within administrator limits;
|
|
- labels use the reserved namespace and cannot be overridden;
|
|
- custom labels are bounded, may not use either `dogama.*` or the internal `io.dogama.*` namespace, and are merged before immutable technical labels;
|
|
- the optional Docker `User` is either an already validated numeric `UID:GID` value or omitted so the image `USER` applies;
|
|
- only approved DoGaMa networks are attached.
|
|
|
|
The canonical plan digest alone is not treated as approval. The agent embeds and
|
|
validates the same catalog, then independently compares every privileged field
|
|
to the pinned immutable template snapshot before checking paths or pulling an
|
|
image. A caller cannot make a substituted image or mount valid merely by
|
|
recomputing a digest.
|
|
|
|
V1 templates do not expose arbitrary Docker security options. The agent applies a secure fixed baseline: no-new-privileges where compatible, dropped capabilities by default, bounded PIDs and a non-host network mode.
|
|
|
|
## Authentication and replay defense
|
|
|
|
Requests use a shared secret read from a Docker secret file. 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
|
|
base64url `X-DoGaMa-Nonce`. The HMAC-SHA-256 input is the following exact
|
|
newline-separated canonical value:
|
|
|
|
```text
|
|
DOGAMA-HMAC-V1
|
|
<HTTP method>
|
|
<escaped URL path>
|
|
<timestamp header>
|
|
<nonce header>
|
|
<lowercase SHA-256 hex digest of the body>
|
|
```
|
|
|
|
Queries are rejected. The accepted clock skew is 30 seconds, a nonce is valid
|
|
once only, and request bodies are limited to 64 KiB before dispatch. The agent
|
|
returns stable JSON problem codes and never includes secrets, socket paths or
|
|
raw Docker errors.
|
|
|
|
The agent listens only on the internal control network and publishes no host port. Authentication remains mandatory even on that network.
|
|
|
|
## Failure semantics
|
|
|
|
- Validate the full plan before pulling or mutating anything.
|
|
- Return typed, non-sensitive errors.
|
|
- Create with a deterministic name only after registration intent is persisted.
|
|
- On partial create, remove the incomplete container but never delete bind-mounted data.
|
|
- On replace failure, retain or recreate the prior container plan when safe; otherwise leave the instance stopped and report intervention required.
|
|
- Agent startup reconciles its registry with Docker but never adopts unknown containers automatically.
|
|
|
|
## Testing focus
|
|
|
|
Negative integration tests must cover forged labels, unknown IDs, path traversal, symlink escape, reserved-label overrides, host networking, privileged flags, extra mounts, image substitution, conflicting ports, replayed requests and attempts to target unrelated containers.
|