diff --git a/docs/adr/model/0020-hardening-is-one-platform-posture.md b/docs/adr/model/0020-hardening-is-one-platform-posture.md index 3abca27e8..ea4633cce 100644 --- a/docs/adr/model/0020-hardening-is-one-platform-posture.md +++ b/docs/adr/model/0020-hardening-is-one-platform-posture.md @@ -29,10 +29,11 @@ listed in [chapter 10](../../../spec/v1/10-project-intent.md#hardening). ## Rests on -Kubernetes applies the four controls (non-root, an immutable root filesystem, no -capabilities, the default syscall filter) per pod and per container +Kubernetes applies the five controls (non-root, an immutable root filesystem, no +capabilities, no privilege escalation, the default syscall filter) per pod and +per container ([0002](0002-kubernetes-is-the-substrate-for-one-applier.md)), so the render can -state all four on every container with no author involved. Ephemeral disk is +state all five on every container with no author involved. Ephemeral disk is contended, so its size is platform-assigned ([0004](0004-contention-decides-authority.md)). The lock is a pinned input ([0006](0006-pinned-inputs.md)), so a user read from it is a derivation, not a @@ -42,7 +43,7 @@ registry read at render time. exception to render, a Process's writable set cannot be enumerated ahead of time, or an image the estate depends on has no numeric user and cannot be replaced. **Settled by:** the committed trees under -`spec/v1/examples/{auth,data,knowledge,minimal}/rendered/` carrying the four +`spec/v1/examples/{auth,data,knowledge,minimal}/rendered/` carrying the five controls on every container, the `postgres-exporter` sidecar included, with no project file carrying a hardening field (a test asserting both is to be written, owned by joris); and `platform-postgres` running `initdb` on a freshly @@ -71,6 +72,13 @@ A fresh `local-path` directory is root-owned, so a non-root pod with no to resolve a digest, so it reads the user too. An init container that chowns the volume would need root on every stateful Process. +**Privilege escalation is closed, not implied.** Non-root with every capability +dropped still lets a setuid binary in the image raise its process to uid 0; +`allowPrivilegeEscalation: false` sets `no_new_privs` and closes that. It is +also one of the controls Kubernetes' own `restricted` Pod Security Standard +requires, so a posture under that name without it would be refused by a +namespace enforcing the standard. + **A low port is refused.** `auth-ui` declared port 80 under `restricted` and rendered a pod that could not bind it. Deriving `NET_BIND_SERVICE` would re-add a dropped capability silently. A route names a surface, not a number diff --git a/spec/v1/10-project-intent.md b/spec/v1/10-project-intent.md index 7d190e033..74c3cd1f3 100644 --- a/spec/v1/10-project-intent.md +++ b/spec/v1/10-project-intent.md @@ -1149,7 +1149,7 @@ The posture itself is **not authored per Process**. It is one estate-wide value, it thirty times would be restating the only value there is, and a field with one legal value carries no information ([0022](../../docs/adr/model/0022-a-derived-value-has-one-declaring-site.md) deleted `minAvailable` for the same reason). What a Process authors is the -paths it must write, and nothing else. `restricted` is four controls, applied +paths it must write, and nothing else. `restricted` is five controls, applied together: | control | rendered as | @@ -1157,6 +1157,7 @@ together: | non-root | `runAsNonRoot: true`, with the numeric UID the images lock resolved ([0020](../../docs/adr/model/0020-hardening-is-one-platform-posture.md)) | | immutable root filesystem | `readOnlyRootFilesystem: true` | | no capabilities | `capabilities.drop: [ALL]` | +| no privilege escalation | `allowPrivilegeEscalation: false` | | default syscall filter | `seccompProfile.type: RuntimeDefault` | ### Writable paths are declared, not exempted diff --git a/spec/v1/20-resolved-deployment.md b/spec/v1/20-resolved-deployment.md index 34cb727b0..4a4a2019d 100644 --- a/spec/v1/20-resolved-deployment.md +++ b/spec/v1/20-resolved-deployment.md @@ -657,8 +657,8 @@ Five rules carry most of the weight: with different volumes, which is the property that makes a restore rehearsal meaningful ([0018](../../docs/adr/model/0018-durability-class-derives-a-backup.md)). - **Hardening is one platform posture, and nothing else.** `restricted` ( - `runAsNonRoot`, `readOnlyRootFilesystem`, all capabilities dropped, seccomp - `RuntimeDefault`) is declared once in the Platform document and authored by no + `runAsNonRoot`, `readOnlyRootFilesystem`, all capabilities dropped, no + privilege escalation, seccomp `RuntimeDefault`) is declared once in the Platform document and authored by no Process. There is no per-control relaxation and no exception vocabulary: a Process declares the paths it must write, and an image that cannot meet the class is `E_HARDENING_UNMET` diff --git a/spec/v1/30-deliverables.md b/spec/v1/30-deliverables.md index 7c8278ffe..9ddcdb1d9 100644 --- a/spec/v1/30-deliverables.md +++ b/spec/v1/30-deliverables.md @@ -330,7 +330,7 @@ spells only what the projection holds: | the `secretStore` endpoint | `vso` | a `VaultConnection` named `secret-store` in the project's namespace, at that address, `skipTLSVerify: false` | | a grant delivered `env` or `file` | `vso` | a `VaultAuth` per holding identity, its `ServiceAccount` that identity and its role `-`, over the `secret-store` connection; a `VaultStaticSecret` named for the `destination`, `kv-v2` on the `secret` mount at the path below `secret/data/`, `refreshAfter: 1h`, its destination created and `flagger.app/config-tracking: disabled`, each restart target a `Deployment`, a `blue-green` Process's `-primary` | | a `read` grant | `vault-policy` | `read` on its path and on the same document's `secret/metadata/` path | -| `hardening: restricted` | `kubernetes` | `runAsNonRoot`, the images lock's `uid` and `gid`, seccomp `RuntimeDefault`, a read-only root filesystem, every capability dropped | +| `hardening: restricted` | `kubernetes` | `runAsNonRoot`, the images lock's `uid` and `gid`, seccomp `RuntimeDefault`, a read-only root filesystem, every capability dropped, `allowPrivilegeEscalation: false` | | a probe's `period`, `timeout`, `failures` | `kubernetes` | `periodSeconds`, `timeoutSeconds`, `failureThreshold`; `initialDelaySeconds: 0` on readiness and liveness only | | `ingress`, `egress`, an edge's `peers` | `networking` | one rule per peer, from or to its namespace (by `kubernetes.io/metadata.name`) and, where the peer is a Process, its `instance`, on TCP; the `cluster-dns` peer on UDP and TCP both | | a held `api`'s `server` | `networking` | one egress rule per range in the Process's own policy, after its peers, to an `ipBlock` of its `cidr` on its port over TCP |