docs(security): document secret files, runtime guard, and hardening
- commit
- 4e3a25d2ce9766b540ce4164a01cff55c64ea9b3
- author
- brice · · 2026-10-08 20:33 UTC
- parent
- 49efad0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +98 -1
M docs/config/apps.md +7 -1
| @@ -52,7 +52,9 @@ port = 8080 # required; no host or tls | |||
| 52 | 52 | LOG_LEVEL = "debug" # overrides an app-wide [env] key of the same name | |
| 53 | 53 | ||
| 54 | 54 | [services.web.secrets] # ENV name -> secret name (values in pass) | |
| 55 | - | DATABASE_URL = "database-url" | |
| 55 | + | API_KEY = "api-key" # env delivery | |
| 56 | + | DATABASE_URL = { name = "database-url", as = "file" } # /run/secrets/DATABASE_URL | |
| 57 | + | DB_PASS = { name = "db-pass", as = "env" } # explicit env delivery | |
| 56 | 58 | ||
| 57 | 59 | [[services.web.volume]] # 0..n named volumes only | |
| 58 | 60 | name = "web-data" | |
| @@ -113,6 +115,10 @@ Image registry names and digest syntax are validated during manifest apply, reso | |||
| 113 | 115 | - `[env]` is app-wide public env injected into all services. Each key must be disjoint from every `[services.<name>.secrets]` key in the app. | |
| 114 | 116 | - `[services.<name>.env]` is public env for that service only. A key here overrides the same key in `[env]` for this service. Each key must be disjoint from the same service's `[services.<name>.secrets]` keys. Values follow the `[env]` rules: no newlines, 1 byte to 64 KiB, no secret references. Values are stored in revisions and shown in plain text, so use secrets for anything sensitive. A change shows as `service/<name>/env` in diffs and applies on the next deploy. | |
| 115 | 117 | - `[services.<name>.secrets]` maps ENV var name to service-local secret name. Values are written with `gordon apps secrets set` and stay in pass under `gordon/apps/<uuid>/<service>/<name>`. Running containers keep the values they were created with; `gordon apps deploy` applies new values. `restart` does not. | |
| 118 | + | - A secret entry is either a secret name string (env delivery) or a table `{ name = "...", as = "env" | "file" }`. `as` defaults to `env`; any other key or value is rejected. | |
| 119 | + | - `as = "file"` delivers the value as a read-only file (mode `0444`) at `/run/secrets/<ENV>` and sets `<ENV>_FILE=/run/secrets/<ENV>` instead of `<ENV>`. The value never appears in the container environment, `docker inspect`, or `/proc/1/environ`. Use it for images that support the `*_FILE` convention. | |
| 120 | + | - With file delivery, `<ENV>_FILE` must not be another secret key or a key in `[env]` or `[services.<name>.env]`, and no bind or volume of the service may mount at or under `/run/secrets`. | |
| 121 | + | - Switching a secret between `env` and `file` shows as `service/<name>/secret/<secret>` in diffs and applies on the next deploy. See [Secrets](./secrets.md#secret-files). | |
| 116 | 122 | ||
| 117 | 123 | ## Volumes and Databases | |
| 118 | 124 | ||
| @@ -52,7 +52,9 @@ port = 8080 # required; no host or tls | |||||
| 52 | LOG_LEVEL = "debug" # overrides an app-wide [env] key of the same name | 52 | LOG_LEVEL = "debug" # overrides an app-wide [env] key of the same name | ||
| 53 | 53 | ||||
| 54 | [services.web.secrets] # ENV name -> secret name (values in pass) | 54 | [services.web.secrets] # ENV name -> secret name (values in pass) | ||
| 55 | - | DATABASE_URL = "database-url" | 55 | + | API_KEY = "api-key" # env delivery |
| 56 | + | DATABASE_URL = { name = "database-url", as = "file" } # /run/secrets/DATABASE_URL | |||
| 57 | + | DB_PASS = { name = "db-pass", as = "env" } # explicit env delivery | |||
| 56 | 58 | ||||
| 57 | [[services.web.volume]] # 0..n named volumes only | 59 | [[services.web.volume]] # 0..n named volumes only | ||
| 58 | name = "web-data" | 60 | name = "web-data" | ||
| @@ -113,6 +115,10 @@ Image registry names and digest syntax are validated during manifest apply, reso | |||||
| 113 | - `[env]` is app-wide public env injected into all services. Each key must be disjoint from every `[services.<name>.secrets]` key in the app. | 115 | - `[env]` is app-wide public env injected into all services. Each key must be disjoint from every `[services.<name>.secrets]` key in the app. | ||
| 114 | - `[services.<name>.env]` is public env for that service only. A key here overrides the same key in `[env]` for this service. Each key must be disjoint from the same service's `[services.<name>.secrets]` keys. Values follow the `[env]` rules: no newlines, 1 byte to 64 KiB, no secret references. Values are stored in revisions and shown in plain text, so use secrets for anything sensitive. A change shows as `service/<name>/env` in diffs and applies on the next deploy. | 116 | - `[services.<name>.env]` is public env for that service only. A key here overrides the same key in `[env]` for this service. Each key must be disjoint from the same service's `[services.<name>.secrets]` keys. Values follow the `[env]` rules: no newlines, 1 byte to 64 KiB, no secret references. Values are stored in revisions and shown in plain text, so use secrets for anything sensitive. A change shows as `service/<name>/env` in diffs and applies on the next deploy. | ||
| 115 | - `[services.<name>.secrets]` maps ENV var name to service-local secret name. Values are written with `gordon apps secrets set` and stay in pass under `gordon/apps/<uuid>/<service>/<name>`. Running containers keep the values they were created with; `gordon apps deploy` applies new values. `restart` does not. | 117 | - `[services.<name>.secrets]` maps ENV var name to service-local secret name. Values are written with `gordon apps secrets set` and stay in pass under `gordon/apps/<uuid>/<service>/<name>`. Running containers keep the values they were created with; `gordon apps deploy` applies new values. `restart` does not. | ||
| 118 | + | - A secret entry is either a secret name string (env delivery) or a table `{ name = "...", as = "env" | "file" }`. `as` defaults to `env`; any other key or value is rejected. | |||
| 119 | + | - `as = "file"` delivers the value as a read-only file (mode `0444`) at `/run/secrets/<ENV>` and sets `<ENV>_FILE=/run/secrets/<ENV>` instead of `<ENV>`. The value never appears in the container environment, `docker inspect`, or `/proc/1/environ`. Use it for images that support the `*_FILE` convention. | |||
| 120 | + | - With file delivery, `<ENV>_FILE` must not be another secret key or a key in `[env]` or `[services.<name>.env]`, and no bind or volume of the service may mount at or under `/run/secrets`. | |||
| 121 | + | - Switching a secret between `env` and `file` shows as `service/<name>/secret/<secret>` in diffs and applies on the next deploy. See [Secrets](./secrets.md#secret-files). | |||
| 116 | 122 | ||||
| 117 | ## Volumes and Databases | 123 | ## Volumes and Databases | ||
| 118 | 124 | ||||
M docs/config/secrets.md +19 -0
| @@ -117,6 +117,22 @@ gordon apps secrets set blog --service web DATABASE_URL=... | |||
| 117 | 117 | ||
| 118 | 118 | Values affect the next deploy/restart, never running containers. Only key names are ever echoed back, never values. See [App Manifest](./apps.md) and [Apps CLI](../cli/apps.md). | |
| 119 | 119 | ||
| 120 | + | ### Secret files | |
| 121 | + | ||
| 122 | + | A secret declared with `as = "file"` is delivered as a file instead of an env value: | |
| 123 | + | ||
| 124 | + | ```toml | |
| 125 | + | [services.web.secrets] | |
| 126 | + | DATABASE_URL = { name = "database-url", as = "file" } | |
| 127 | + | ``` | |
| 128 | + | ||
| 129 | + | - The container sees a read-only file at `/run/secrets/DATABASE_URL` and the env var `DATABASE_URL_FILE=/run/secrets/DATABASE_URL`. | |
| 130 | + | - On the host, Gordon writes one file per secret under `<data_dir>/secrets-run/<app>/<container>/`, with directories `0700` and files `0444`. The files persist so containers restarted by the runtime or after a reboot still find them. | |
| 131 | + | - Gordon removes a container's files once the container is confirmed gone, and sweeps files of unknown containers at startup. | |
| 132 | + | - Exclude `<data_dir>/secrets-run` from host backups: it holds plain secret values. | |
| 133 | + | - Env-delivered secrets stay visible through `docker inspect` and `/proc/1/environ` inside the container. Prefer file delivery for apps that read `*_FILE` variables. | |
| 134 | + | - An older Gordon v3 binary ignores file delivery and injects those secrets as env vars again. Its leftover `secrets-run` directories are harmless and are swept by the next start of a version that supports file delivery. | |
| 135 | + | ||
| 120 | 136 | ## Examples | |
| 121 | 137 | ||
| 122 | 138 | ### Production with Pass | |
| @@ -152,9 +168,12 @@ secrets_backend = "unsafe" | |||
| 152 | 168 | 3. **Rotate regularly**: Regenerate tokens and passwords periodically | |
| 153 | 169 | 4. **Least privilege**: Use separate secrets per environment | |
| 154 | 170 | 5. **Path validation**: SOPS provider rejects absolute paths and path traversal attempts for security | |
| 171 | + | 6. **File delivery**: Use `as = "file"` for app secrets when the image supports `*_FILE` variables (see [Secret files](#secret-files)) | |
| 172 | + | 7. **Hardening**: See [Security Hardening](./security-hardening.md) for the runtime guard, process protection, and credential scoping | |
| 155 | 173 | ||
| 156 | 174 | ## Related | |
| 157 | 175 | ||
| 158 | 176 | - [Authentication](./auth.md) | |
| 159 | 177 | - [App Manifest](./apps.md) | |
| 178 | + | - [Security Hardening](./security-hardening.md) | |
| 160 | 179 | - [Configuration Overview](./index.md) | |
| @@ -117,6 +117,22 @@ gordon apps secrets set blog --service web DATABASE_URL=... | |||||
| 117 | 117 | ||||
| 118 | Values affect the next deploy/restart, never running containers. Only key names are ever echoed back, never values. See [App Manifest](./apps.md) and [Apps CLI](../cli/apps.md). | 118 | Values affect the next deploy/restart, never running containers. Only key names are ever echoed back, never values. See [App Manifest](./apps.md) and [Apps CLI](../cli/apps.md). | ||
| 119 | 119 | ||||
| 120 | + | ### Secret files | |||
| 121 | + | ||||
| 122 | + | A secret declared with `as = "file"` is delivered as a file instead of an env value: | |||
| 123 | + | ||||
| 124 | + | ```toml | |||
| 125 | + | [services.web.secrets] | |||
| 126 | + | DATABASE_URL = { name = "database-url", as = "file" } | |||
| 127 | + | ``` | |||
| 128 | + | ||||
| 129 | + | - The container sees a read-only file at `/run/secrets/DATABASE_URL` and the env var `DATABASE_URL_FILE=/run/secrets/DATABASE_URL`. | |||
| 130 | + | - On the host, Gordon writes one file per secret under `<data_dir>/secrets-run/<app>/<container>/`, with directories `0700` and files `0444`. The files persist so containers restarted by the runtime or after a reboot still find them. | |||
| 131 | + | - Gordon removes a container's files once the container is confirmed gone, and sweeps files of unknown containers at startup. | |||
| 132 | + | - Exclude `<data_dir>/secrets-run` from host backups: it holds plain secret values. | |||
| 133 | + | - Env-delivered secrets stay visible through `docker inspect` and `/proc/1/environ` inside the container. Prefer file delivery for apps that read `*_FILE` variables. | |||
| 134 | + | - An older Gordon v3 binary ignores file delivery and injects those secrets as env vars again. Its leftover `secrets-run` directories are harmless and are swept by the next start of a version that supports file delivery. | |||
| 135 | + | ||||
| 120 | ## Examples | 136 | ## Examples | ||
| 121 | 137 | ||||
| 122 | ### Production with Pass | 138 | ### Production with Pass | ||
| @@ -152,9 +168,12 @@ secrets_backend = "unsafe" | |||||
| 152 | 3. **Rotate regularly**: Regenerate tokens and passwords periodically | 168 | 3. **Rotate regularly**: Regenerate tokens and passwords periodically | ||
| 153 | 4. **Least privilege**: Use separate secrets per environment | 169 | 4. **Least privilege**: Use separate secrets per environment | ||
| 154 | 5. **Path validation**: SOPS provider rejects absolute paths and path traversal attempts for security | 170 | 5. **Path validation**: SOPS provider rejects absolute paths and path traversal attempts for security | ||
| 171 | + | 6. **File delivery**: Use `as = "file"` for app secrets when the image supports `*_FILE` variables (see [Secret files](#secret-files)) | |||
| 172 | + | 7. **Hardening**: See [Security Hardening](./security-hardening.md) for the runtime guard, process protection, and credential scoping | |||
| 155 | 173 | ||||
| 156 | ## Related | 174 | ## Related | ||
| 157 | 175 | ||||
| 158 | - [Authentication](./auth.md) | 176 | - [Authentication](./auth.md) | ||
| 159 | - [App Manifest](./apps.md) | 177 | - [App Manifest](./apps.md) | ||
| 178 | + | - [Security Hardening](./security-hardening.md) | |||
| 160 | - [Configuration Overview](./index.md) | 179 | - [Configuration Overview](./index.md) | ||
M docs/config/security-hardening.md +70 -0
| @@ -172,9 +172,79 @@ security_profile = "compat" # or "strict" | |||
| 172 | 172 | ||
| 173 | 173 | Use `strict` for images designed to write only to mounted volumes and run without extra Linux capabilities. | |
| 174 | 174 | ||
| 175 | + | ## Runtime guard | |
| 176 | + | ||
| 177 | + | Every app container config passes a final guard right before the runtime creates the container. A refused config creates no container and fails with error code `runtime-guard-violation` (HTTP 400). 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 | + | ||
| 183 | + | Protected paths: | |
| 184 | + | ||
| 185 | + | - the data directory and the loaded config file; | |
| 186 | + | - 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; | |
| 187 | + | - Gordon runtime credentials: `$XDG_RUNTIME_DIR/gordon` and `~/.gordon/run`; | |
| 188 | + | - key stores: `$GNUPGHOME`, `~/.gnupg`, `$PASSWORD_STORE_DIR`, `~/.password-store`, `$SOPS_AGE_KEY_FILE`, and `${XDG_CONFIG_HOME:-~/.config}/sops/age`. | |
| 189 | + | ||
| 190 | + | The only 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 is reported at deploy preflight, before the running generation is replaced. Point the policy `source` outside every protected path. | |
| 191 | + | ||
| 192 | + | ## Process memory protection | |
| 193 | + | ||
| 194 | + | On Linux, `gordon serve` marks itself non-dumpable (`PR_SET_DUMPABLE=0`) before loading any configuration or 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. | |
| 195 | + | ||
| 196 | + | This does not stop root, nor a process of the same user that can replace the Gordon binary or its systemd unit. | |
| 197 | + | ||
| 198 | + | ## Hardened systemd user unit | |
| 199 | + | ||
| 200 | + | Add these directives to the `[Service]` section of the [user unit](../installation.md#user-service-recommended-for-rootless): | |
| 201 | + | ||
| 202 | + | ```ini | |
| 203 | + | [Service] | |
| 204 | + | NoNewPrivileges=yes | |
| 205 | + | UMask=0077 | |
| 206 | + | LockPersonality=yes | |
| 207 | + | RestrictSUIDSGID=yes | |
| 208 | + | RestrictRealtime=yes | |
| 209 | + | SystemCallArchitectures=native | |
| 210 | + | ``` | |
| 211 | + | ||
| 212 | + | Apply and check: | |
| 213 | + | ||
| 214 | + | ```bash | |
| 215 | + | systemctl --user daemon-reload | |
| 216 | + | systemctl --user restart gordon | |
| 217 | + | systemctl --user status gordon | |
| 218 | + | systemd-analyze --user security gordon.service | |
| 219 | + | ``` | |
| 220 | + | ||
| 221 | + | ## Credential scoping | |
| 222 | + | ||
| 223 | + | - **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). | |
| 224 | + | - **Registry push tokens**: issue one token per repository and pipeline instead of a global one: | |
| 225 | + | ||
| 226 | + | ```bash | |
| 227 | + | gordon auth token generate --subject ci-blog --scopes push --repo blog | |
| 228 | + | ``` | |
| 229 | + | ||
| 230 | + | ## Protection boundary | |
| 231 | + | ||
| 232 | + | Gordon v3 protects against: | |
| 233 | + | ||
| 234 | + | - file-delivered secret values showing up in container env, `docker inspect`, or `/proc/1/environ`; | |
| 235 | + | - app configs that mount the runtime socket, Gordon data, config, credentials, or key stores, or that join the host or another container's namespaces; | |
| 236 | + | - same-user processes reading Gordon's memory or environment through `/proc`. | |
| 237 | + | ||
| 238 | + | Gordon v3 does not protect against: | |
| 239 | + | ||
| 240 | + | - a compromised Gordon process, proxy, or registry: they run as the same user that owns the runtime socket and `pass`; | |
| 241 | + | - a malicious image deployed for an app: it receives that app's secrets; | |
| 242 | + | - root on the host. | |
| 243 | + | ||
| 175 | 244 | ## Related | |
| 176 | 245 | ||
| 177 | 246 | - [Auth](./auth.md) | |
| 247 | + | - [Secrets](./secrets.md) | |
| 178 | 248 | - [Images](./images.md) | |
| 179 | 249 | - [Network Isolation](./network-isolation.md) | |
| 180 | 250 | - [Volumes](./volumes.md) | |
| @@ -172,9 +172,79 @@ security_profile = "compat" # or "strict" | |||||
| 172 | 172 | ||||
| 173 | Use `strict` for images designed to write only to mounted volumes and run without extra Linux capabilities. | 173 | Use `strict` for images designed to write only to mounted volumes and run without extra Linux capabilities. | ||
| 174 | 174 | ||||
| 175 | + | ## Runtime guard | |||
| 176 | + | ||||
| 177 | + | Every app container config passes a final guard right before the runtime creates the container. A refused config creates no container and fails with error code `runtime-guard-violation` (HTTP 400). 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 | + | ||||
| 183 | + | Protected paths: | |||
| 184 | + | ||||
| 185 | + | - the data directory and the loaded config file; | |||
| 186 | + | - 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; | |||
| 187 | + | - Gordon runtime credentials: `$XDG_RUNTIME_DIR/gordon` and `~/.gordon/run`; | |||
| 188 | + | - key stores: `$GNUPGHOME`, `~/.gnupg`, `$PASSWORD_STORE_DIR`, `~/.password-store`, `$SOPS_AGE_KEY_FILE`, and `${XDG_CONFIG_HOME:-~/.config}/sops/age`. | |||
| 189 | + | ||||
| 190 | + | The only 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 is reported at deploy preflight, before the running generation is replaced. Point the policy `source` outside every protected path. | |||
| 191 | + | ||||
| 192 | + | ## Process memory protection | |||
| 193 | + | ||||
| 194 | + | On Linux, `gordon serve` marks itself non-dumpable (`PR_SET_DUMPABLE=0`) before loading any configuration or 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. | |||
| 195 | + | ||||
| 196 | + | This does not stop root, nor a process of the same user that can replace the Gordon binary or its systemd unit. | |||
| 197 | + | ||||
| 198 | + | ## Hardened systemd user unit | |||
| 199 | + | ||||
| 200 | + | Add these directives to the `[Service]` section of the [user unit](../installation.md#user-service-recommended-for-rootless): | |||
| 201 | + | ||||
| 202 | + | ```ini | |||
| 203 | + | [Service] | |||
| 204 | + | NoNewPrivileges=yes | |||
| 205 | + | UMask=0077 | |||
| 206 | + | LockPersonality=yes | |||
| 207 | + | RestrictSUIDSGID=yes | |||
| 208 | + | RestrictRealtime=yes | |||
| 209 | + | SystemCallArchitectures=native | |||
| 210 | + | ``` | |||
| 211 | + | ||||
| 212 | + | Apply and check: | |||
| 213 | + | ||||
| 214 | + | ```bash | |||
| 215 | + | systemctl --user daemon-reload | |||
| 216 | + | systemctl --user restart gordon | |||
| 217 | + | systemctl --user status gordon | |||
| 218 | + | systemd-analyze --user security gordon.service | |||
| 219 | + | ``` | |||
| 220 | + | ||||
| 221 | + | ## Credential scoping | |||
| 222 | + | ||||
| 223 | + | - **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). | |||
| 224 | + | - **Registry push tokens**: issue one token per repository and pipeline instead of a global one: | |||
| 225 | + | ||||
| 226 | + | ```bash | |||
| 227 | + | gordon auth token generate --subject ci-blog --scopes push --repo blog | |||
| 228 | + | ``` | |||
| 229 | + | ||||
| 230 | + | ## Protection boundary | |||
| 231 | + | ||||
| 232 | + | Gordon v3 protects against: | |||
| 233 | + | ||||
| 234 | + | - file-delivered secret values showing up in container env, `docker inspect`, or `/proc/1/environ`; | |||
| 235 | + | - app configs that mount the runtime socket, Gordon data, config, credentials, or key stores, or that join the host or another container's namespaces; | |||
| 236 | + | - same-user processes reading Gordon's memory or environment through `/proc`. | |||
| 237 | + | ||||
| 238 | + | Gordon v3 does not protect against: | |||
| 239 | + | ||||
| 240 | + | - a compromised Gordon process, proxy, or registry: they run as the same user that owns the runtime socket and `pass`; | |||
| 241 | + | - a malicious image deployed for an app: it receives that app's secrets; | |||
| 242 | + | - root on the host. | |||
| 243 | + | ||||
| 175 | ## Related | 244 | ## Related | ||
| 176 | 245 | ||||
| 177 | - [Auth](./auth.md) | 246 | - [Auth](./auth.md) | ||
| 247 | + | - [Secrets](./secrets.md) | |||
| 178 | - [Images](./images.md) | 248 | - [Images](./images.md) | ||
| 179 | - [Network Isolation](./network-isolation.md) | 249 | - [Network Isolation](./network-isolation.md) | ||
| 180 | - [Volumes](./volumes.md) | 250 | - [Volumes](./volumes.md) | ||
M docs/installation.md +2 -0
| @@ -266,6 +266,8 @@ sudo loginctl enable-linger $USER | |||
| 266 | 266 | systemctl --user status gordon | |
| 267 | 267 | ``` | |
| 268 | 268 | ||
| 269 | + | See [Hardened systemd user unit](./config/security-hardening.md#hardened-systemd-user-unit) for the recommended sandboxing directives. | |
| 270 | + | ||
| 269 | 271 | ### System Service (Root Mode) | |
| 270 | 272 | ||
| 271 | 273 | ```bash | |
| @@ -266,6 +266,8 @@ sudo loginctl enable-linger $USER | |||||
| 266 | systemctl --user status gordon | 266 | systemctl --user status gordon | ||
| 267 | ``` | 267 | ``` | ||
| 268 | 268 | ||||
| 269 | + | See [Hardened systemd user unit](./config/security-hardening.md#hardened-systemd-user-unit) for the recommended sandboxing directives. | |||
| 270 | + | ||||
| 269 | ### System Service (Root Mode) | 271 | ### System Service (Root Mode) | ||
| 270 | 272 | ||||
| 271 | ```bash | 273 | ```bash | ||