security-hardening.md RawHistoryFolder

Security Hardening Controls

This page summarizes Gordon's security-related configuration knobs and the defaults operators should know before exposing an instance.

Registry upload quotas

Registry blob uploads are bounded by two limits under [server]:

[server]
max_blob_chunk_size = "95MB"
max_blob_size = "1GB"
  • max_blob_chunk_size limits a single upload chunk.
  • max_blob_size limits the cumulative blob/layer upload size.
  • Exceeding either limit returns an OCI-compatible size error and the failed upload is cleaned up.

Admin logs permission

Container and deploy logs can include environment-derived output. Gordon gates log access behind a dedicated scope:

gordon auth token generate --subject ops --scopes "admin:logs:read" --expiry 30d
  • /admin/logs, app failure diagnostics, and deploy failure logs require admin:logs:read.
  • admin:status:read does not grant log access.
  • Operation errors returned to admin:apps:read callers are stable and log-free. Application diagnostics are a separate field, redacted of the affected service's resolved secret values before storage, and returned only to callers holding admin:logs:read. If a declared secret cannot be resolved for redaction, Gordon drops the diagnostics instead of storing unredacted output. Mutation responses never include application output.
  • Process and container log sources remain operator-owned data. Gordon redacts common credential patterns when serving logs through its API, but this is defense in depth rather than proof that arbitrary application output is secret-free. Restrict filesystem, journal, runtime, backup, and admin:logs:read access accordingly.

Volume pruning scope

Volume pruning removes a volume only when Gordon has a durable released ownership record, the runtime app/incarnation/service labels agree with that record, and no container mounts it. A gordon.managed=true label by itself is not deletion authority; retained, unknown, contradictory, and unrelated volumes survive.

Use gordon volumes prune --dry-run before deletion. Do not substitute docker volume prune or an equivalent runtime command: runtime-native pruning bypasses Gordon's ownership and retention checks.

Use dedicated admin scopes:

  • admin:volumes:read for listing volumes.
  • admin:volumes:write for prune operations.

Administrative app bind mounts

App manifests cannot name host paths. A host bind exists only when the operator declares a named policy in gordon.toml:

[app_mounts.app-logs]
source = "/srv/gordon/host-logs"
read_only = true
allowed_apps = ["metrics-agent"]    # exact, non-empty
allowed_services = ["web"]          # exact, non-empty
root = "/srv/gordon"                # optional boundary

The manifest references only the policy name: [[services.<name>.bind]] name = "app-logs" with an absolute container path.

  • allowed_apps and allowed_services are exact, non-empty allowlists, so least privilege is enforced by construction. There is no wildcard, prefix, or empty-means-all form.
  • source is resolved through symlinks and must be a regular file or directory under root. When root is omitted, the source's parent directory is the boundary. Devices, sockets, FIFOs, and escaping symlinks are refused.
  • Read-only precedence: read_only on the policy or readonly on the manifest bind forces the mount read-only; a manifest never weakens its policy.
  • Destinations must be absolute, normalized, and outside reserved container paths (/, /proc, /sys, /dev, /boot, and their children).
  • The policy is re-resolved immediately before every container create, restart, and recovery. Removing a policy or an allowlist entry blocks future deploys; running containers keep their current mounts until redeployed.
  • gordon serve reload republishes validated policies atomically. An edit that fails validation is rejected and the previous policies stay live.
  • Gordon never creates, deletes, chowns, backs up, or prunes the host path; ownership, permissions, and backup remain the operator's responsibility.

Administrative app devices (CDI)

App manifests cannot name host devices. A device grant exists only when the operator declares a named policy in gordon.toml:

[app_devices.transcode-gpu]
cdi = ["example.com/gpu=GPU-device-uuid"]
allowed_apps = ["video"]          # exact, non-empty
allowed_services = ["transcoder"]  # exact, non-empty

The manifest references only the logical name: devices = ["transcode-gpu"].

  • cdi holds explicit CDI device IDs. Raw /dev paths, unqualified names, and aggregate =all selectors are rejected.
  • allowed_apps and allowed_services are exact, non-empty allowlists, so least privilege is enforced by construction. There is no wildcard, prefix, or empty-means-all form.
  • Gordon resolves logical names to CDI IDs at activation time and encodes them as one native CDI DeviceRequest. Revisions persist the logical names, never the host resolution.
  • The grant is re-resolved immediately before every preflight, container create, restart, and recovery. Revoking a grant blocks future deploys while the running service is untouched; changing a mapping never recreates a running container.
  • Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured. Older or unrecognized engines fail with a structured runtime-unsupported error and never run without devices.
  • Gordon installs no drivers, manages no quotas, and injects no device environment: images carry their own runtime expectations.

External image registries

Docker Hub, ghcr.io, quay.io, and Gordon's configured registry are always allowed. Add every other registry hostname and non-default port explicitly:

[images]
allowed_registries = ["registry.internal:5000"]
require_digest = true
  • docker.io and Docker Hub's canonical pull host registry-1.docker.io are equivalent.
  • Other registries are accepted only when their exact hostname+port is configured. Allowlisting does not configure credentials; external resolution and pulls are anonymous unless the runtime already has suitable access.
  • require_digest = true requires every image, including Gordon registry images, to use @sha256:<64 hex chars>.
  • The allowlist restricts hostnames, not resolved IPs. It cannot prove a DNS hostname is non-private and does not replace firewall or runtime egress controls.

Smart TCP Raw Fallback

Raw TCP fallback on a smart_tcp edge entrypoint is disabled by default. Unknown non-HTTP/non-TLS bytes reach a backend only when the entrypoint explicitly sets raw_fallback to a TCP router and the fallback source policy allows the peer.

[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"
raw_fallback = "ssh-fallback"
raw_fallback_trusted_cidrs = ["100.64.0.0/10"]

Use raw_fallback_trusted_cidrs for private raw fallback. Public raw fallback requires an explicit acknowledgement:

[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"
raw_fallback = "public-raw"
allow_public_raw_fallback = true

Security behavior:

  • PROXY protocol v1 and v2 prefixes are rejected; Gordon does not trust or parse PROXY headers on smart TCP entrypoints.
  • trusted_cidrs and raw_fallback_trusted_cidrs use the peer socket IP, not X-Forwarded-For.
  • HTTP-looking or TLS-looking malformed traffic is rejected and never bypasses to raw fallback.
  • Entry-point-wide trusted_cidrs applies before sniffing to all protocols; raw fallback has its own narrower policy for private fallback exposure.

Docker network isolation

Per-app Docker networks are enabled by default. To block direct external egress from isolated networks, opt into Docker internal networks:

[network_isolation]
enabled = true
internal = true

internal = false remains the compatibility default because some applications need direct egress during startup.

Every app container joins an incarnation-owned private network derived from the app's internal UUID; shared memberships come only from explicit [[network.shared]] declarations, and memory/CPU/PID limits apply on every create and recovery path.

Readiness helper containers

Probing an internal HTTP port uses one bounded helper container per readiness attempt, removed immediately after. Gordon force-removes the helper under an independent cleanup context, including on failure and timeout. There is no operator configuration for the helper; its image and limits are fixed.

  • The helper image is digest-pinned and multi-arch: alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc. The image must be pre-provisioned and available offline on the target host, because the probe never pulls.
  • The helper attaches only to the target app's private network. It never joins a shared network or a host network.
  • It never publishes a host port and has no mounts, volumes, secrets, environment, or runtime socket.
  • It runs non-root (uid/gid 65534) with a read-only root filesystem, all capabilities dropped, no-new-privileges, and bounded CPU, memory, and PIDs.
  • It targets the exact inspected container IP on that network, never a service alias, and revalidates the container's execution start before trusting the result; a restarted generation is discarded.
  • A target that listens only on its own loopback fails readiness.
  • Helpers left behind by an abrupt daemon or host stop are reclaimed by the next probe once they are older than ten minutes; a live session is never touched.

Registry exposure

  • With auth.enabled = true, registry requests are authenticated and repository-scoped; the public proxy forwards registry domains to the internal registry.
  • With auth.enabled = false, the registry is local-only: the public proxy refuses registry-domain requests, and the registry handler accepts only direct loopback connections carrying the instance credentials.
  • Registry requests are parsed once into a validated operation used by both authorization and dispatch, so the repository a token is checked against is exactly the repository served.
  • Blob and upload access is repository-scoped: an upload UUID is usable only by the repository that started it, and a blob is served only to a repository that completed an upload of that digest.

Container runtime profile

[containers]
security_profile = "compat" # or "strict"
  • compat preserves broad image compatibility while retaining no-new-privileges and default capability restrictions.
  • strict enables a read-only root filesystem, drops all capabilities, and only adds NET_BIND_SERVICE.

Use strict for images designed to write only to mounted volumes and run without extra Linux capabilities.

Runtime guard

Every app container config passes a final guard right before the runtime creates the container. Bind checks also run at deploy preflight and before every restart or recovery of an existing container. A refused config creates or starts no container: the operation fails and its preflight or service step error contains runtime guard violation. The guard refuses:

  • network mode host, container:<id>, or ns:<path>;
  • a volume name containing / or \, which the runtime would read as a host path;
  • a bind whose host source is equal to, under, or a parent of a protected path, compared both as written and after resolving symlinks.

Protected paths:

  • the data directory and the loaded config file;
  • files and directories set in the config: server.tls_key_file and the backup storage_dir values;
  • runtime sockets: /var/run/docker.sock, /run/docker.sock, /run/podman/podman.sock, $XDG_RUNTIME_DIR/docker.sock, $XDG_RUNTIME_DIR/podman/podman.sock, the unix:// path of DOCKER_HOST, and the server.runtime socket;
  • rootless runtime storage and config: ~/.local/share/containers, ~/.local/share/docker, ${XDG_CONFIG_HOME:-~/.config}/containers, and ~/.docker;
  • the systemd user units: ${XDG_CONFIG_HOME:-~/.config}/systemd/user;
  • Gordon runtime credentials: $XDG_RUNTIME_DIR/gordon, ~/.gordon/run, and $GORDON_CLOUDFLARE_API_TOKEN_FILE;
  • key stores: $GNUPGHOME, ~/.gnupg, $PASSWORD_STORE_DIR, ~/.password-store, $SOPS_AGE_KEY_FILE, and ${XDG_CONFIG_HOME:-~/.config}/sops/age.

A protected path that does not exist yet is also protected in its symlink-resolved form, based on its closest existing parent. Backup directories are protected: a backup app cannot bind them, even read-only.

Gordon's enabled file logs (the Gordon log file's directory, the access log's directory, and logging.container_logs.dir) can be bound read-only so a log shipper can read them. Under the data directory this applies only at or under <data_dir>/logs; outside it, only when the directory neither is, contains, nor lies under another protected path. A writable bind on them is refused. When a log directory does not qualify, for example a log file directly in $HOME, the log file itself stays protected and no read-only exception applies.

The only other exception is Gordon's own read-only secret file mount: <data_dir>/secrets-run/<app>/<container>/<KEY> at /run/secrets/<KEY>. A refused [app_mounts] policy fails before the running generation is replaced or restarted. Point the policy source outside every protected path.

Process memory protection

On Linux, gordon serve marks itself non-dumpable (PR_SET_DUMPABLE=0) as its first step, before loading its config file or any backend secret. Other processes of the same user can no longer read its /proc/<pid>/environ, mem, or file descriptors, attach with ptrace, or get a core dump. Startup fails if the flag cannot be set.

Limits:

  • Secrets passed as environment variables, such as GORDON_AUTH_TOKEN_SECRET, are readable for the short time between process start and that first step. Prefer a secrets backend.
  • Helper processes (pass, gpg, sops, journalctl) are dumpable again after exec; secret tools hold the values they decrypt while they run. Gordon starts them without its GORDON_* variables.
  • Root, or a process of the same user that can replace the Gordon binary or its systemd unit, is not stopped.

Hardened systemd user unit

Add these directives to the [Service] section of the user unit:

[Service]
NoNewPrivileges=yes
UMask=0077
LockPersonality=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
SystemCallArchitectures=native

Apply and check:

systemctl --user daemon-reload
systemctl --user restart gordon
systemctl --user status gordon
systemd-analyze --user security gordon.service

Credential scoping

  • Cloudflare DNS-01 token: grant only Zone:Read and DNS:Edit, limited to the zones Gordon serves. A leaked token can issue certificates for, and take over, every hostname in those zones. See Server.

  • Registry push tokens: issue one token per repository and pipeline instead of a global one:

    gordon auth token generate --subject ci-blog --scopes push --repo blog
    

Protection boundary

Gordon v3 protects against:

  • file-delivered secret values showing up in container env, docker inspect, or /proc/1/environ;
  • app configs that mount the runtime socket, Gordon data, config, credentials, or key stores, or that join the host or another container's namespaces;
  • same-user processes reading the Gordon process's memory or environment through /proc, within the limits above.

Gordon v3 does not protect against:

  • a compromised Gordon process, proxy, or registry: they run as the same user that owns the runtime socket and pass;
  • a malicious image deployed for an app: it receives that app's secrets;
  • root on the host.
source
1# Security Hardening Controls
2
3This page summarizes Gordon's security-related configuration knobs and the defaults operators should know before exposing an instance.
4
5## Registry upload quotas
6
7Registry blob uploads are bounded by two limits under `[server]`:
8
9```toml
10[server]
11max_blob_chunk_size = "95MB"
12max_blob_size = "1GB"
13```
14
15- `max_blob_chunk_size` limits a single upload chunk.
16- `max_blob_size` limits the cumulative blob/layer upload size.
17- Exceeding either limit returns an OCI-compatible size error and the failed upload is cleaned up.
18
19## Admin logs permission
20
21Container and deploy logs can include environment-derived output. Gordon gates log access behind a dedicated scope:
22
23```bash
24gordon auth token generate --subject ops --scopes "admin:logs:read" --expiry 30d
25```
26
27- `/admin/logs`, app failure diagnostics, and deploy failure logs require `admin:logs:read`.
28- `admin:status:read` does not grant log access.
29- Operation errors returned to `admin:apps:read` callers are stable and log-free. Application diagnostics are a separate field, redacted of the affected service's resolved secret values before storage, and returned only to callers holding `admin:logs:read`. If a declared secret cannot be resolved for redaction, Gordon drops the diagnostics instead of storing unredacted output. Mutation responses never include application output.
30- Process and container log sources remain operator-owned data. Gordon redacts common credential patterns when serving logs through its API, but this is defense in depth rather than proof that arbitrary application output is secret-free. Restrict filesystem, journal, runtime, backup, and `admin:logs:read` access accordingly.
31
32## Volume pruning scope
33
34Volume pruning removes a volume only when Gordon has a durable `released` ownership record, the runtime app/incarnation/service labels agree with that record, and no container mounts it. A `gordon.managed=true` label by itself is not deletion authority; retained, unknown, contradictory, and unrelated volumes survive.
35
36Use `gordon volumes prune --dry-run` before deletion. Do not substitute `docker volume prune` or an equivalent runtime command: runtime-native pruning bypasses Gordon's ownership and retention checks.
37
38Use dedicated admin scopes:
39
40- `admin:volumes:read` for listing volumes.
41- `admin:volumes:write` for prune operations.
42
43## Administrative app bind mounts
44
45App manifests cannot name host paths. A host bind exists only when the operator declares a named policy in `gordon.toml`:
46
47```toml
48[app_mounts.app-logs]
49source = "/srv/gordon/host-logs"
50read_only = true
51allowed_apps = ["metrics-agent"]    # exact, non-empty
52allowed_services = ["web"]          # exact, non-empty
53root = "/srv/gordon"                # optional boundary
54```
55
56The manifest references only the policy name: `[[services.<name>.bind]] name = "app-logs"` with an absolute container `path`.
57
58- `allowed_apps` and `allowed_services` are exact, non-empty allowlists, so least privilege is enforced by construction. There is no wildcard, prefix, or empty-means-all form.
59- `source` is resolved through symlinks and must be a regular file or directory under `root`. When `root` is omitted, the source's parent directory is the boundary. Devices, sockets, FIFOs, and escaping symlinks are refused.
60- Read-only precedence: `read_only` on the policy or `readonly` on the manifest bind forces the mount read-only; a manifest never weakens its policy.
61- Destinations must be absolute, normalized, and outside reserved container paths (`/`, `/proc`, `/sys`, `/dev`, `/boot`, and their children).
62- The policy is re-resolved immediately before every container create, restart, and recovery. Removing a policy or an allowlist entry blocks future deploys; running containers keep their current mounts until redeployed.
63- `gordon serve` reload republishes validated policies atomically. An edit that fails validation is rejected and the previous policies stay live.
64- Gordon never creates, deletes, chowns, backs up, or prunes the host path; ownership, permissions, and backup remain the operator's responsibility.
65
66## Administrative app devices (CDI)
67
68App manifests cannot name host devices. A device grant exists only when the operator declares a named policy in `gordon.toml`:
69
70```toml
71[app_devices.transcode-gpu]
72cdi = ["example.com/gpu=GPU-device-uuid"]
73allowed_apps = ["video"]          # exact, non-empty
74allowed_services = ["transcoder"]  # exact, non-empty
75```
76
77The manifest references only the logical name: `devices = ["transcode-gpu"]`.
78
79- `cdi` holds explicit CDI device IDs. Raw `/dev` paths, unqualified names, and aggregate `=all` selectors are rejected.
80- `allowed_apps` and `allowed_services` are exact, non-empty allowlists, so least privilege is enforced by construction. There is no wildcard, prefix, or empty-means-all form.
81- Gordon resolves logical names to CDI IDs at activation time and encodes them as one native CDI `DeviceRequest`. Revisions persist the logical names, never the host resolution.
82- The grant is re-resolved immediately before every preflight, container create, restart, and recovery. Revoking a grant blocks future deploys while the running service is untouched; changing a mapping never recreates a running container.
83- Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured. Older or unrecognized engines fail with a structured `runtime-unsupported` error and never run without devices.
84- Gordon installs no drivers, manages no quotas, and injects no device environment: images carry their own runtime expectations.
85
86## External image registries
87
88Docker Hub, `ghcr.io`, `quay.io`, and Gordon's configured registry are always allowed. Add every other registry hostname and non-default port explicitly:
89
90```toml
91[images]
92allowed_registries = ["registry.internal:5000"]
93require_digest = true
94```
95
96- `docker.io` and Docker Hub's canonical pull host `registry-1.docker.io` are equivalent.
97- Other registries are accepted only when their exact hostname+port is configured. Allowlisting does not configure credentials; external resolution and pulls are anonymous unless the runtime already has suitable access.
98- `require_digest = true` requires every image, including Gordon registry images, to use `@sha256:<64 hex chars>`.
99- The allowlist restricts hostnames, not resolved IPs. It cannot prove a DNS hostname is non-private and does not replace firewall or runtime egress controls.
100
101## Smart TCP Raw Fallback
102
103Raw TCP fallback on a `smart_tcp` edge entrypoint is disabled by default. Unknown non-HTTP/non-TLS bytes reach a backend only when the entrypoint explicitly sets `raw_fallback` to a TCP router and the fallback source policy allows the peer.
104
105```toml
106[entrypoints.edge]
107address = ":443"
108protocol = "smart_tcp"
109raw_fallback = "ssh-fallback"
110raw_fallback_trusted_cidrs = ["100.64.0.0/10"]
111```
112
113Use `raw_fallback_trusted_cidrs` for private raw fallback. Public raw fallback requires an explicit acknowledgement:
114
115```toml
116[entrypoints.edge]
117address = ":443"
118protocol = "smart_tcp"
119raw_fallback = "public-raw"
120allow_public_raw_fallback = true
121```
122
123Security behavior:
124
125- PROXY protocol v1 and v2 prefixes are rejected; Gordon does not trust or parse PROXY headers on smart TCP entrypoints.
126- `trusted_cidrs` and `raw_fallback_trusted_cidrs` use the peer socket IP, not `X-Forwarded-For`.
127- HTTP-looking or TLS-looking malformed traffic is rejected and never bypasses to raw fallback.
128- Entry-point-wide `trusted_cidrs` applies before sniffing to all protocols; raw fallback has its own narrower policy for private fallback exposure.
129
130## Docker network isolation
131
132Per-app Docker networks are enabled by default. To block direct external egress from isolated networks, opt into Docker internal networks:
133
134```toml
135[network_isolation]
136enabled = true
137internal = true
138```
139
140`internal = false` remains the compatibility default because some applications need direct egress during startup.
141
142Every app container joins an incarnation-owned private network derived from the app's internal UUID; shared memberships come only from explicit `[[network.shared]]` declarations, and memory/CPU/PID limits apply on every create and recovery path.
143
144## Readiness helper containers
145
146Probing an internal HTTP port uses one bounded helper container per readiness attempt, removed immediately after. Gordon force-removes the helper under an independent cleanup context, including on failure and timeout. There is no operator configuration for the helper; its image and limits are fixed.
147
148- The helper image is digest-pinned and multi-arch: `alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc`. The image must be pre-provisioned and available offline on the target host, because the probe never pulls.
149- The helper attaches only to the target app's private network. It never joins a shared network or a host network.
150- It never publishes a host port and has no mounts, volumes, secrets, environment, or runtime socket.
151- It runs non-root (uid/gid `65534`) with a read-only root filesystem, all capabilities dropped, `no-new-privileges`, and bounded CPU, memory, and PIDs.
152- It targets the exact inspected container IP on that network, never a service alias, and revalidates the container's execution start before trusting the result; a restarted generation is discarded.
153- A target that listens only on its own loopback fails readiness.
154- Helpers left behind by an abrupt daemon or host stop are reclaimed by the next probe once they are older than ten minutes; a live session is never touched.
155
156## Registry exposure
157
158- With `auth.enabled = true`, registry requests are authenticated and repository-scoped; the public proxy forwards registry domains to the internal registry.
159- With `auth.enabled = false`, the registry is local-only: the public proxy refuses registry-domain requests, and the registry handler accepts only direct loopback connections carrying the instance credentials.
160- Registry requests are parsed once into a validated operation used by both authorization and dispatch, so the repository a token is checked against is exactly the repository served.
161- Blob and upload access is repository-scoped: an upload UUID is usable only by the repository that started it, and a blob is served only to a repository that completed an upload of that digest.
162
163## Container runtime profile
164
165```toml
166[containers]
167security_profile = "compat" # or "strict"
168```
169
170- `compat` preserves broad image compatibility while retaining `no-new-privileges` and default capability restrictions.
171- `strict` enables a read-only root filesystem, drops all capabilities, and only adds `NET_BIND_SERVICE`.
172
173Use `strict` for images designed to write only to mounted volumes and run without extra Linux capabilities.
174
175## Runtime guard
176
177Every app container config passes a final guard right before the runtime creates the container. Bind checks also run at deploy preflight and before every restart or recovery of an existing container. A refused config creates or starts no container: the operation fails and its preflight or service step error contains `runtime guard violation`. The guard refuses:
178
179- network mode `host`, `container:<id>`, or `ns:<path>`;
180- a volume name containing `/` or `\`, which the runtime would read as a host path;
181- a bind whose host source is equal to, under, or a parent of a protected path, compared both as written and after resolving symlinks.
182
183Protected paths:
184
185- the data directory and the loaded config file;
186- files and directories set in the config: `server.tls_key_file` and the backup `storage_dir` values;
187- runtime sockets: `/var/run/docker.sock`, `/run/docker.sock`, `/run/podman/podman.sock`, `$XDG_RUNTIME_DIR/docker.sock`, `$XDG_RUNTIME_DIR/podman/podman.sock`, the `unix://` path of `DOCKER_HOST`, and the `server.runtime` socket;
188- rootless runtime storage and config: `~/.local/share/containers`, `~/.local/share/docker`, `${XDG_CONFIG_HOME:-~/.config}/containers`, and `~/.docker`;
189- the systemd user units: `${XDG_CONFIG_HOME:-~/.config}/systemd/user`;
190- Gordon runtime credentials: `$XDG_RUNTIME_DIR/gordon`, `~/.gordon/run`, and `$GORDON_CLOUDFLARE_API_TOKEN_FILE`;
191- key stores: `$GNUPGHOME`, `~/.gnupg`, `$PASSWORD_STORE_DIR`, `~/.password-store`, `$SOPS_AGE_KEY_FILE`, and `${XDG_CONFIG_HOME:-~/.config}/sops/age`.
192
193A protected path that does not exist yet is also protected in its symlink-resolved form, based on its closest existing parent. Backup directories are protected: a backup app cannot bind them, even read-only.
194
195Gordon's enabled file logs (the Gordon log file's directory, the access log's directory, and `logging.container_logs.dir`) can be bound read-only so a log shipper can read them. Under the data directory this applies only at or under `<data_dir>/logs`; outside it, only when the directory neither is, contains, nor lies under another protected path. A writable bind on them is refused. When a log directory does not qualify, for example a log file directly in `$HOME`, the log file itself stays protected and no read-only exception applies.
196
197The only other exception is Gordon's own read-only secret file mount: `<data_dir>/secrets-run/<app>/<container>/<KEY>` at `/run/secrets/<KEY>`. A refused `[app_mounts]` policy fails before the running generation is replaced or restarted. Point the policy `source` outside every protected path.
198
199## Process memory protection
200
201On Linux, `gordon serve` marks itself non-dumpable (`PR_SET_DUMPABLE=0`) as its first step, before loading its config file or any backend secret. Other processes of the same user can no longer read its `/proc/<pid>/environ`, `mem`, or file descriptors, attach with ptrace, or get a core dump. Startup fails if the flag cannot be set.
202
203Limits:
204
205- Secrets passed as environment variables, such as `GORDON_AUTH_TOKEN_SECRET`, are readable for the short time between process start and that first step. Prefer a secrets backend.
206- Helper processes (`pass`, `gpg`, `sops`, `journalctl`) are dumpable again after `exec`; secret tools hold the values they decrypt while they run. Gordon starts them without its `GORDON_*` variables.
207- Root, or a process of the same user that can replace the Gordon binary or its systemd unit, is not stopped.
208
209## Hardened systemd user unit
210
211Add these directives to the `[Service]` section of the [user unit](../installation.md#user-service-recommended-for-rootless):
212
213```ini
214[Service]
215NoNewPrivileges=yes
216UMask=0077
217LockPersonality=yes
218RestrictSUIDSGID=yes
219RestrictRealtime=yes
220SystemCallArchitectures=native
221```
222
223Apply and check:
224
225```bash
226systemctl --user daemon-reload
227systemctl --user restart gordon
228systemctl --user status gordon
229systemd-analyze --user security gordon.service
230```
231
232## Credential scoping
233
234- **Cloudflare DNS-01 token**: grant only `Zone:Read` and `DNS:Edit`, limited to the zones Gordon serves. A leaked token can issue certificates for, and take over, every hostname in those zones. See [Server](./server.md).
235- **Registry push tokens**: issue one token per repository and pipeline instead of a global one:
236
237  ```bash
238  gordon auth token generate --subject ci-blog --scopes push --repo blog
239  ```
240
241## Protection boundary
242
243Gordon v3 protects against:
244
245- file-delivered secret values showing up in container env, `docker inspect`, or `/proc/1/environ`;
246- app configs that mount the runtime socket, Gordon data, config, credentials, or key stores, or that join the host or another container's namespaces;
247- same-user processes reading the Gordon process's memory or environment through `/proc`, within the limits above.
248
249Gordon v3 does not protect against:
250
251- a compromised Gordon process, proxy, or registry: they run as the same user that owns the runtime socket and `pass`;
252- a malicious image deployed for an app: it receives that app's secrets;
253- root on the host.
254
255## Related
256
257- [Auth](./auth.md)
258- [Secrets](./secrets.md)
259- [Images](./images.md)
260- [Network Isolation](./network-isolation.md)
261- [Volumes](./volumes.md)
262- [Reference](./reference.md)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close