fix(security): strip GORDON_ env from journalctl and keep TLS cert dir bindable
- commit
- b4f4f5a45aa467aa32cdb13655b5ccacba53388c
- author
- brice · · 2026-10-08 21:15 UTC
- parent
- 64d925b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +26 -28
M docs/config/security-hardening.md +4 -4
| @@ -174,7 +174,7 @@ Use `strict` for images designed to write only to mounted volumes and run withou | |||
| 174 | 174 | ||
| 175 | 175 | ## Runtime guard | |
| 176 | 176 | ||
| 177 | - | 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`. A replayed deploy whose preflight failed this way returns error code `runtime-guard-violation` (HTTP 400). The guard refuses: | |
| 177 | + | 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: | |
| 178 | 178 | ||
| 179 | 179 | - network mode `host`, `container:<id>`, or `ns:<path>`; | |
| 180 | 180 | - a volume name containing `/` or `\`, which the runtime would read as a host path; | |
| @@ -183,14 +183,14 @@ Every app container config passes a final guard right before the runtime creates | |||
| 183 | 183 | Protected paths: | |
| 184 | 184 | ||
| 185 | 185 | - the data directory and the loaded config file; | |
| 186 | - | - files and directories set in the config: `server.tls_cert_file`, `server.tls_key_file`, the Gordon log file, `logging.container_logs.dir`, `logging.access_log.file_path`, and the backup `storage_dir` values; | |
| 186 | + | - files and directories set in the config: `server.tls_key_file`, the Gordon log file, `logging.container_logs.dir`, `logging.access_log.file_path`, and the backup `storage_dir` values; | |
| 187 | 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 | 188 | - rootless runtime storage and config: `~/.local/share/containers`, `~/.local/share/docker`, `${XDG_CONFIG_HOME:-~/.config}/containers`, and `~/.docker`; | |
| 189 | 189 | - the systemd user units: `${XDG_CONFIG_HOME:-~/.config}/systemd/user`; | |
| 190 | 190 | - Gordon runtime credentials: `$XDG_RUNTIME_DIR/gordon`, `~/.gordon/run`, and `$GORDON_CLOUDFLARE_API_TOKEN_FILE`; | |
| 191 | 191 | - key stores: `$GNUPGHOME`, `~/.gnupg`, `$PASSWORD_STORE_DIR`, `~/.password-store`, `$SOPS_AGE_KEY_FILE`, and `${XDG_CONFIG_HOME:-~/.config}/sops/age`. | |
| 192 | 192 | ||
| 193 | - | A protected path that does not exist yet is also protected in its symlink-resolved form, based on its closest existing parent. | |
| 193 | + | A protected path that does not exist yet is also protected in its symlink-resolved form, based on its closest existing parent. Container log and backup directories are protected too: a log-shipper or backup app cannot bind them, even read-only. | |
| 194 | 194 | ||
| 195 | 195 | 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 fails before the running generation is replaced or restarted. Point the policy `source` outside every protected path. | |
| 196 | 196 | ||
| @@ -201,7 +201,7 @@ On Linux, `gordon serve` marks itself non-dumpable (`PR_SET_DUMPABLE=0`) as its | |||
| 201 | 201 | Limits: | |
| 202 | 202 | ||
| 203 | 203 | - 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. | |
| 204 | - | - Helper processes (`pass`, `gpg`, `sops`) are dumpable again after `exec` and hold the values they decrypt while they run. Gordon starts them without its `GORDON_*` variables. | |
| 204 | + | - 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. | |
| 205 | 205 | - Root, or a process of the same user that can replace the Gordon binary or its systemd unit, is not stopped. | |
| 206 | 206 | ||
| 207 | 207 | ## Hardened systemd user unit | |
| @@ -174,7 +174,7 @@ Use `strict` for images designed to write only to mounted volumes and run withou | |||||
| 174 | 174 | ||||
| 175 | ## Runtime guard | 175 | ## Runtime guard | ||
| 176 | 176 | ||||
| 177 | - | 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`. A replayed deploy whose preflight failed this way returns error code `runtime-guard-violation` (HTTP 400). The guard refuses: | 177 | + | 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: |
| 178 | 178 | ||||
| 179 | - network mode `host`, `container:<id>`, or `ns:<path>`; | 179 | - network mode `host`, `container:<id>`, or `ns:<path>`; | ||
| 180 | - a volume name containing `/` or `\`, which the runtime would read as a host path; | 180 | - a volume name containing `/` or `\`, which the runtime would read as a host path; | ||
| @@ -183,14 +183,14 @@ Every app container config passes a final guard right before the runtime creates | |||||
| 183 | Protected paths: | 183 | Protected paths: | ||
| 184 | 184 | ||||
| 185 | - the data directory and the loaded config file; | 185 | - the data directory and the loaded config file; | ||
| 186 | - | - files and directories set in the config: `server.tls_cert_file`, `server.tls_key_file`, the Gordon log file, `logging.container_logs.dir`, `logging.access_log.file_path`, and the backup `storage_dir` values; | 186 | + | - files and directories set in the config: `server.tls_key_file`, the Gordon log file, `logging.container_logs.dir`, `logging.access_log.file_path`, 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; | 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`; | 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`; | 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`; | 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`. | 191 | - key stores: `$GNUPGHOME`, `~/.gnupg`, `$PASSWORD_STORE_DIR`, `~/.password-store`, `$SOPS_AGE_KEY_FILE`, and `${XDG_CONFIG_HOME:-~/.config}/sops/age`. | ||
| 192 | 192 | ||||
| 193 | - | A protected path that does not exist yet is also protected in its symlink-resolved form, based on its closest existing parent. | 193 | + | A protected path that does not exist yet is also protected in its symlink-resolved form, based on its closest existing parent. Container log and backup directories are protected too: a log-shipper or backup app cannot bind them, even read-only. |
| 194 | 194 | ||||
| 195 | 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 fails before the running generation is replaced or restarted. Point the policy `source` outside every protected path. | 195 | 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 fails before the running generation is replaced or restarted. Point the policy `source` outside every protected path. | ||
| 196 | 196 | ||||
| @@ -201,7 +201,7 @@ On Linux, `gordon serve` marks itself non-dumpable (`PR_SET_DUMPABLE=0`) as its | |||||
| 201 | Limits: | 201 | Limits: | ||
| 202 | 202 | ||||
| 203 | - 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. | 203 | - 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. | ||
| 204 | - | - Helper processes (`pass`, `gpg`, `sops`) are dumpable again after `exec` and hold the values they decrypt while they run. Gordon starts them without its `GORDON_*` variables. | 204 | + | - 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. |
| 205 | - Root, or a process of the same user that can replace the Gordon binary or its systemd unit, is not stopped. | 205 | - Root, or a process of the same user that can replace the Gordon binary or its systemd unit, is not stopped. | ||
| 206 | 206 | ||||
| 207 | ## Hardened systemd user unit | 207 | ## Hardened systemd user unit | ||
M internal/adapters/in/http/admin/handler_apps.go +0 -1
| @@ -539,7 +539,6 @@ func isMappedPreflightError(err error, op *domain.AppOperation) bool { | |||
| 539 | 539 | // Split from isMappedPreflightError to keep its complexity within budget. | |
| 540 | 540 | func isPreflightErrorRest(err error) bool { | |
| 541 | 541 | return errors.Is(err, domain.ErrRuntimeUnsupported) || | |
| 542 | - | errors.Is(err, domain.ErrRuntimeGuard) || | |
| 543 | 542 | errors.Is(err, domain.ErrAppNotFound) || | |
| 544 | 543 | errors.Is(err, domain.ErrAppReservationConflict) || | |
| 545 | 544 | errors.Is(err, domain.ErrAppImageUnresolvable) || | |
| @@ -539,7 +539,6 @@ func isMappedPreflightError(err error, op *domain.AppOperation) bool { | |||||
| 539 | // Split from isMappedPreflightError to keep its complexity within budget. | 539 | // Split from isMappedPreflightError to keep its complexity within budget. | ||
| 540 | func isPreflightErrorRest(err error) bool { | 540 | func isPreflightErrorRest(err error) bool { | ||
| 541 | return errors.Is(err, domain.ErrRuntimeUnsupported) || | 541 | return errors.Is(err, domain.ErrRuntimeUnsupported) || | ||
| 542 | - | errors.Is(err, domain.ErrRuntimeGuard) || | |||
| 543 | errors.Is(err, domain.ErrAppNotFound) || | 542 | errors.Is(err, domain.ErrAppNotFound) || | ||
| 544 | errors.Is(err, domain.ErrAppReservationConflict) || | 543 | errors.Is(err, domain.ErrAppReservationConflict) || | ||
| 545 | errors.Is(err, domain.ErrAppImageUnresolvable) || | 544 | errors.Is(err, domain.ErrAppImageUnresolvable) || | ||
M internal/adapters/in/http/admin/handler_apps_test.go +7 -16
| @@ -682,24 +682,15 @@ func TestHandler_AppApply_MapsDevicePolicyViolation(t *testing.T) { | |||
| 682 | 682 | assert.Equal(t, "device-policy-violation", envelope.Error) | |
| 683 | 683 | } | |
| 684 | 684 | ||
| 685 | - | // TestHandler_AppDeploy_MapsRuntimeGuardPreflightReplay proves a terminal | |
| 686 | - | // replay whose preflight failed on the runtime guard maps to the stable | |
| 687 | - | // runtime-guard-violation envelope. | |
| 688 | - | func TestHandler_AppDeploy_MapsRuntimeGuardPreflightReplay(t *testing.T) { | |
| 689 | - | appSvc := inmocks.NewMockAppService(t) | |
| 690 | - | handler := appsTestHandler(t, appSvc) | |
| 685 | + | // TestSendAppOpError_MapsRuntimeGuard proves a runtime guard error returned | |
| 686 | + | // synchronously maps to the stable runtime-guard-violation envelope. Async | |
| 687 | + | // deploys report the guard in their operation journal instead. | |
| 688 | + | func TestSendAppOpError_MapsRuntimeGuard(t *testing.T) { | |
| 689 | + | handler := appsTestHandler(t, inmocks.NewMockAppService(t)) | |
| 690 | + | rec := httptest.NewRecorder() | |
| 691 | 691 | ||
| 692 | - | op := &domain.AppOperation{ | |
| 693 | - | Op: "op-g", Kind: "deploy", App: "blog", Outcome: domain.AppOutcomeFailed, | |
| 694 | - | Steps: []domain.AppOperationStep{{ID: "preflight", State: domain.AppStepFailed, Error: "runtime guard violation"}}, | |
| 695 | - | } | |
| 696 | - | appSvc.EXPECT().Deploy(mock.Anything, "blog", "", "", false, "test-operation-key").Return( | |
| 697 | - | op, | |
| 698 | - | fmt.Errorf("deployment: app %q service %q: %w: bind %q overlaps a protected Gordon path", "blog", "web", domain.ErrRuntimeGuard, "conf"), | |
| 699 | - | ).Once() | |
| 692 | + | handler.sendAppOpError(rec, fmt.Errorf("deployment: app %q service %q: %w: bind %q overlaps a protected Gordon path", "blog", "web", domain.ErrRuntimeGuard, "conf")) | |
| 700 | 693 | ||
| 701 | - | rec := appsRequest(t, handler, http.MethodPost, "/admin/apps/blog/deploy", | |
| 702 | - | dto.AppDeployRequest{}, "admin:apps:write") | |
| 703 | 694 | assert.Equal(t, http.StatusBadRequest, rec.Code) | |
| 704 | 695 | var envelope dto.AppError | |
| 705 | 696 | require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope)) | |
| @@ -682,24 +682,15 @@ func TestHandler_AppApply_MapsDevicePolicyViolation(t *testing.T) { | |||||
| 682 | assert.Equal(t, "device-policy-violation", envelope.Error) | 682 | assert.Equal(t, "device-policy-violation", envelope.Error) | ||
| 683 | } | 683 | } | ||
| 684 | 684 | ||||
| 685 | - | // TestHandler_AppDeploy_MapsRuntimeGuardPreflightReplay proves a terminal | 685 | + | // TestSendAppOpError_MapsRuntimeGuard proves a runtime guard error returned |
| 686 | - | // replay whose preflight failed on the runtime guard maps to the stable | 686 | + | // synchronously maps to the stable runtime-guard-violation envelope. Async |
| 687 | - | // runtime-guard-violation envelope. | 687 | + | // deploys report the guard in their operation journal instead. |
| 688 | - | func TestHandler_AppDeploy_MapsRuntimeGuardPreflightReplay(t *testing.T) { | 688 | + | func TestSendAppOpError_MapsRuntimeGuard(t *testing.T) { |
| 689 | - | appSvc := inmocks.NewMockAppService(t) | 689 | + | handler := appsTestHandler(t, inmocks.NewMockAppService(t)) |
| 690 | - | handler := appsTestHandler(t, appSvc) | 690 | + | rec := httptest.NewRecorder() |
| 691 | 691 | ||||
| 692 | - | op := &domain.AppOperation{ | 692 | + | handler.sendAppOpError(rec, fmt.Errorf("deployment: app %q service %q: %w: bind %q overlaps a protected Gordon path", "blog", "web", domain.ErrRuntimeGuard, "conf")) |
| 693 | - | Op: "op-g", Kind: "deploy", App: "blog", Outcome: domain.AppOutcomeFailed, | |||
| 694 | - | Steps: []domain.AppOperationStep{{ID: "preflight", State: domain.AppStepFailed, Error: "runtime guard violation"}}, | |||
| 695 | - | } | |||
| 696 | - | appSvc.EXPECT().Deploy(mock.Anything, "blog", "", "", false, "test-operation-key").Return( | |||
| 697 | - | op, | |||
| 698 | - | fmt.Errorf("deployment: app %q service %q: %w: bind %q overlaps a protected Gordon path", "blog", "web", domain.ErrRuntimeGuard, "conf"), | |||
| 699 | - | ).Once() | |||
| 700 | 693 | ||||
| 701 | - | rec := appsRequest(t, handler, http.MethodPost, "/admin/apps/blog/deploy", | |||
| 702 | - | dto.AppDeployRequest{}, "admin:apps:write") | |||
| 703 | assert.Equal(t, http.StatusBadRequest, rec.Code) | 694 | assert.Equal(t, http.StatusBadRequest, rec.Code) | ||
| 704 | var envelope dto.AppError | 695 | var envelope dto.AppError | ||
| 705 | require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope)) | 696 | require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope)) | ||
M internal/adapters/out/appsecrets/store.go +1 -1
| @@ -13,8 +13,8 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/zerowrap" | |
| 15 | 15 | ||
| 16 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |
| 17 | 16 | "github.com/bnema/gordon/internal/domain" | |
| 17 | + | "github.com/bnema/gordon/pkg/childenv" | |
| 18 | 18 | ) | |
| 19 | 19 | ||
| 20 | 20 | // Store writes app secret values through the pass CLI. | |
| @@ -13,8 +13,8 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | "github.com/bnema/zerowrap" | 14 | "github.com/bnema/zerowrap" | ||
| 15 | 15 | ||||
| 16 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |||
| 17 | "github.com/bnema/gordon/internal/domain" | 16 | "github.com/bnema/gordon/internal/domain" | ||
| 17 | + | "github.com/bnema/gordon/pkg/childenv" | |||
| 18 | ) | 18 | ) | ||
| 19 | 19 | ||||
| 20 | // Store writes app secret values through the pass CLI. | 20 | // Store writes app secret values through the pass CLI. | ||
M internal/adapters/out/secrets/pass.go +1 -1
| @@ -11,8 +11,8 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/zerowrap" | |
| 13 | 13 | ||
| 14 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |
| 15 | 14 | "github.com/bnema/gordon/internal/domain" | |
| 15 | + | "github.com/bnema/gordon/pkg/childenv" | |
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | 18 | // passPathRegex validates pass paths. | |
| @@ -11,8 +11,8 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/bnema/zerowrap" | 12 | "github.com/bnema/zerowrap" | ||
| 13 | 13 | ||||
| 14 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |||
| 15 | "github.com/bnema/gordon/internal/domain" | 14 | "github.com/bnema/gordon/internal/domain" | ||
| 15 | + | "github.com/bnema/gordon/pkg/childenv" | |||
| 16 | ) | 16 | ) | ||
| 17 | 17 | ||||
| 18 | // passPathRegex validates pass paths. | 18 | // passPathRegex validates pass paths. | ||
M internal/adapters/out/secrets/sops.go +1 -1
| @@ -11,7 +11,7 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/zerowrap" | |
| 13 | 13 | ||
| 14 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |
| 14 | + | "github.com/bnema/gordon/pkg/childenv" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | 17 | // sopsKeyPathRegex validates sops key paths (alphanumeric, dots, underscores, hyphens, brackets). | |
| @@ -11,7 +11,7 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/bnema/zerowrap" | 12 | "github.com/bnema/zerowrap" | ||
| 13 | 13 | ||||
| 14 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | 14 | + | "github.com/bnema/gordon/pkg/childenv" |
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | // sopsKeyPathRegex validates sops key paths (alphanumeric, dots, underscores, hyphens, brackets). | 17 | // sopsKeyPathRegex validates sops key paths (alphanumeric, dots, underscores, hyphens, brackets). | ||
M internal/adapters/out/tokenstore/pass.go +1 -1
| @@ -14,8 +14,8 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | "github.com/bnema/zerowrap" | |
| 16 | 16 | ||
| 17 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |
| 18 | 17 | "github.com/bnema/gordon/internal/domain" | |
| 18 | + | "github.com/bnema/gordon/pkg/childenv" | |
| 19 | 19 | ) | |
| 20 | 20 | ||
| 21 | 21 | // ansiRegex matches ANSI escape sequences for stripping from pass output. | |
| @@ -14,8 +14,8 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| 16 | 16 | ||||
| 17 | - | "github.com/bnema/gordon/internal/adapters/out/childenv" | |||
| 18 | "github.com/bnema/gordon/internal/domain" | 17 | "github.com/bnema/gordon/internal/domain" | ||
| 18 | + | "github.com/bnema/gordon/pkg/childenv" | |||
| 19 | ) | 19 | ) | ||
| 20 | 20 | ||||
| 21 | // ansiRegex matches ANSI escape sequences for stripping from pass output. | 21 | // ansiRegex matches ANSI escape sequences for stripping from pass output. | ||
M internal/app/runtime_guard.go +3 -2
| @@ -102,11 +102,12 @@ func homePath(home string, elems ...string) string { | |||
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | 104 | // configuredGordonPaths returns the config file, the runtime socket in use, | |
| 105 | - | // and every file or directory the config points Gordon at. | |
| 105 | + | // and every private file or directory the config points Gordon at. The | |
| 106 | + | // public TLS certificate is left out so its directory (often a shared CA | |
| 107 | + | // bundle) stays bindable. | |
| 106 | 108 | func configuredGordonPaths(si *serviceInit) []string { | |
| 107 | 109 | cfg := si.cfg | |
| 108 | 110 | paths := []string{ | |
| 109 | - | cfg.Server.TLSCertFile, | |
| 110 | 111 | cfg.Server.TLSKeyFile, | |
| 111 | 112 | resolveLogFilePath(cfg), | |
| 112 | 113 | cfg.Logging.ContainerLogs.Dir, | |
| @@ -102,11 +102,12 @@ func homePath(home string, elems ...string) string { | |||||
| 102 | } | 102 | } | ||
| 103 | 103 | ||||
| 104 | // configuredGordonPaths returns the config file, the runtime socket in use, | 104 | // configuredGordonPaths returns the config file, the runtime socket in use, | ||
| 105 | - | // and every file or directory the config points Gordon at. | 105 | + | // and every private file or directory the config points Gordon at. The |
| 106 | + | // public TLS certificate is left out so its directory (often a shared CA | |||
| 107 | + | // bundle) stays bindable. | |||
| 106 | func configuredGordonPaths(si *serviceInit) []string { | 108 | func configuredGordonPaths(si *serviceInit) []string { | ||
| 107 | cfg := si.cfg | 109 | cfg := si.cfg | ||
| 108 | paths := []string{ | 110 | paths := []string{ | ||
| 109 | - | cfg.Server.TLSCertFile, | |||
| 110 | cfg.Server.TLSKeyFile, | 111 | cfg.Server.TLSKeyFile, | ||
| 111 | resolveLogFilePath(cfg), | 112 | resolveLogFilePath(cfg), | ||
| 112 | cfg.Logging.ContainerLogs.Dir, | 113 | cfg.Logging.ContainerLogs.Dir, | ||
M internal/usecase/logs/service.go +8 -1
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | ||
| 19 | 19 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 20 | 20 | "github.com/bnema/gordon/internal/domain" | |
| 21 | + | "github.com/bnema/gordon/pkg/childenv" | |
| 21 | 22 | ) | |
| 22 | 23 | ||
| 23 | 24 | // appStateProvider resolves app services from the durable ACTIVE record. | |
| @@ -35,7 +36,13 @@ type Service struct { | |||
| 35 | 36 | appState appStateProvider | |
| 36 | 37 | } | |
| 37 | 38 | ||
| 38 | - | var execCommandContext = exec.CommandContext | |
| 39 | + | // execCommandContext starts journalctl without Gordon's GORDON_* variables: | |
| 40 | + | // the child is dumpable again after exec, so it must not carry secrets. | |
| 41 | + | var execCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd { | |
| 42 | + | cmd := exec.CommandContext(ctx, name, args...) // #nosec G204 -- callers pass "journalctl" with internal args only | |
| 43 | + | cmd.Env = childenv.Env() | |
| 44 | + | return cmd | |
| 45 | + | } | |
| 39 | 46 | ||
| 40 | 47 | // NewService creates a new log service. App state is wired after the store opens. | |
| 41 | 48 | func NewService( | |
| @@ -18,6 +18,7 @@ import ( | |||||
| 18 | 18 | ||||
| 19 | "github.com/bnema/gordon/internal/boundaries/out" | 19 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 20 | "github.com/bnema/gordon/internal/domain" | 20 | "github.com/bnema/gordon/internal/domain" | ||
| 21 | + | "github.com/bnema/gordon/pkg/childenv" | |||
| 21 | ) | 22 | ) | ||
| 22 | 23 | ||||
| 23 | // appStateProvider resolves app services from the durable ACTIVE record. | 24 | // appStateProvider resolves app services from the durable ACTIVE record. | ||
| @@ -35,7 +36,13 @@ type Service struct { | |||||
| 35 | appState appStateProvider | 36 | appState appStateProvider | ||
| 36 | } | 37 | } | ||
| 37 | 38 | ||||
| 38 | - | var execCommandContext = exec.CommandContext | 39 | + | // execCommandContext starts journalctl without Gordon's GORDON_* variables: |
| 40 | + | // the child is dumpable again after exec, so it must not carry secrets. | |||
| 41 | + | var execCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd { | |||
| 42 | + | cmd := exec.CommandContext(ctx, name, args...) // #nosec G204 -- callers pass "journalctl" with internal args only | |||
| 43 | + | cmd.Env = childenv.Env() | |||
| 44 | + | return cmd | |||
| 45 | + | } | |||
| 39 | 46 | ||||
| 40 | // NewService creates a new log service. App state is wired after the store opens. | 47 | // NewService creates a new log service. App state is wired after the store opens. | ||
| 41 | func NewService( | 48 | func NewService( | ||
R internal/adapters/out/childenv/childenv.go → pkg/childenv/childenv.go +0 -0 renamed, 100% similar
renamed, 100% similar
R internal/adapters/out/childenv/childenv_test.go → pkg/childenv/childenv_test.go +0 -0 renamed, 100% similar
renamed, 100% similar