Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/adr/deferred/examples/reapply-cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ spec:
restartPolicy: OnFailure
# The same identity as the merge apply, so the re-apply cannot touch
# anything the merge apply could not.
applicationAccountName: deployer-auth-federation
serviceAccountName: deployer-auth-federation
containers:
- name: reapply
image: ghcr.io/jorisjonkers-dev/deploy-config-schema:1.0.0
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0004-contention-decides-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ the change that introduced them was being made. One hostname,
three repositories: `homelab-inventory/catalog/reachability.yml`, three
`fleet-infra` edge and knowledge manifests, a bearer-token secret, and the
application's own `platform/deployment.yml`, plus hardcoded in
`ApplicationPermission.kt`. Two conformance tests exist for no purpose other than
`ServicePermission.kt`. Two conformance tests exist for no purpose other than
detecting when those seven disagree. The guard was cheaper to write than the
fix, which is how the estate arrived here.

Expand Down
4 changes: 2 additions & 2 deletions docs/adr/model/0011-configuration-env-files-per-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ three rightful owners: **app knobs** (`SPRING_PROFILES_ACTIVE`,
`KNOWLEDGE_MODE=lite`, uncontended, application-owned literals), **dependency
coordinates** (`DB_HOST`, `DB_PORT`, `RABBITMQ_HOST`, entirely derivable from
`dependsOn`), and **runtime boilerplate** (ten `OTEL_*` variables byte-identical
across `auth-api`, `agents-api` and `knowledge-api` except `OTEL_APPLICATION_NAME`;
across `auth-api`, `agents-api` and `knowledge-api` except `OTEL_SERVICE_NAME`;
`knowledge-ingest-worker`, being Python, carries a different but equally fixed
set, two Runtime Profiles, one derived value, sixty duplicated lines).

Expand Down Expand Up @@ -89,7 +89,7 @@ env file.
| option | cost if taken | why rejected |
|---|---|---|
| Per-Application env files (the old ADRs' scoping) | Processes share an environment they do not have (`knowledge-api` and `knowledge-ingest-worker` overlap on RabbitMQ coordinates and nothing else) and the Process-level joins catching dead grants and unauthorised secret references lose their subject | Finding X4: spec and examples were already per Process; the joins are the only checks between a `secrets` list and an unauthorised read |
| Typed source-declaring map in `application.yml` | A new schema for what dotenv already expresses; a format the estate has zero instances of | The estate's only real config-bearing `.env` file is already dotenv |
| Typed source-declaring map in `service.yml` | A new schema for what dotenv already expresses; a format the estate has zero instances of | The estate's only real config-bearing `.env` file is already dotenv |
| Defaulted-but-overridable derived values | Every override must be audited against staleness by hand | A permitted override is indistinguishable from a stale copy |
| General template language in env files | Configuration becomes a program; values stop being statically derivable and diffable | Placeholders are named-source references, nothing else |

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0014-probes-are-siblings.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ window, and read
falls back to the other. The endpoints are the part only the owning Application
knows ([0005](0005-derivation-is-total.md)); timings, thresholds and deadlines
stay derived ([0030](0030-runtime-mechanics-derived.md)). The v2 model instead
made liveness a fallback (`src/adapters/kubernetes-process-fragment.ts:166`
made liveness a fallback (`src/adapters/kubernetes-workload-fragment.ts:166`
renders `livenessProbe: probe(health.livenessPath ?? health.path)`), and two
live processes rely on it: `app-ui` declares only `/`, `agents-login` only
`/healthz`. For both, liveness silently probes the readiness endpoint.
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0016-pod-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The vocabulary does not exist and neither renderer emits the fields.
src/ schemas/` returns **0 hits** (verified 2026-08-31; the grep spans *both*
renderer generations, so choosing one under
[0052](0052-registered-adapters-are-v1.md) does not rescue it).
`src/deployment/render/processes.ts:130` builds a container from name, image,
`src/deployment/render/workloads.ts:130` builds a container from name, image,
pullPolicy, ports, command, args, env, envFrom, volumeMounts, probes and
resources, and stops. Rendered pods run as their image's UID, with a writable
root filesystem and the default capability set.
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/model/0022-grants-live-on-the-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ identity presenting it to the store is per Process
([0024](0024-identity-per-process.md)), so the level a grant is written at is
the level the store enforces. False if: two Processes of one Application
authenticate as the same principal. Settled by: render a two-Process Application
into the lab cluster, confirm `.spec.applicationAccountName` differs between the two
into the lab cluster, confirm `.spec.serviceAccountName` differs between the two
pods, then exchange each ServiceAccount token for a store token and
`vault kv get` the *sibling's* path, the decision falls if that read succeeds.
This claim inherits [0009](0009-vault-read-is-per-path.md) only through
Expand Down Expand Up @@ -62,7 +62,7 @@ split is already achieved by the env-file placeholder
beside the `dependsOn` edges that motivate them, where a reviewer looks.

The two levels are an access boundary **only** because identity is per Process.
At review time they were not one: `applicationAccountName()` in
At review time they were not one: `serviceAccountName()` in
`src/adapters/kubernetes.ts:665-669` returns `applicationName` for any Application
holding a non-Kubernetes secret, and the previous `16-dependencies.md` derived
the account from `id` to match. Two Processes of one Application therefore
Expand Down
6 changes: 3 additions & 3 deletions docs/adr/model/0024-identity-per-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ a role can bind below the ServiceAccount (to a Pod name, label or controller)
and give two Pods of one ServiceAccount different policies. Settled by: `vault
read auth/kubernetes/role/<role>` and the parameters its create path accepts on
the pinned Vault version, the claim falls if any binding parameter selects finer
than `bound_application_account_names` × `bound_application_account_namespaces`; then the
than `bound_service_account_names` × `bound_service_account_namespaces`; then the
review's tiebreaker, rendering the two-Process `knowledge` example and counting
ServiceAccounts and Vault roles, which must be two of each.

Expand All @@ -41,7 +41,7 @@ boundary. The old credential-provisioning record let a grant sit on the Applicat
(*"every Process receives them"*) or on a Process (*"only it does"*),
while `spec/v1/16-dependencies.md:104` and `:122` derive the ServiceAccount from
`id`, an Application field, drawing `d_id --> k_sa`. The implementation agrees:
`src/adapters/kubernetes.ts:665-669`, `applicationAccountName`, returns
`src/adapters/kubernetes.ts:665-669`, `serviceAccountName`, returns
`applicationName`, one ServiceAccount per Application. Two Processes of one Application
therefore authenticated as the same Vault principal and received the union of
both policies regardless of which level the grant was declared at. Verifying the
Expand Down Expand Up @@ -81,7 +81,7 @@ enforced where a reader can check it.

## Reversibility

Undo cost today: one adapter function (`applicationAccountName`,
Undo cost today: one adapter function (`serviceAccountName`,
`src/adapters/kubernetes.ts:665-669`), the Vault role and policy derivation
beside it, and `## Process identity` in `../../spec/v1/16-dependencies.md`,
hours, blast radius is object count, not authoring. Becomes irreversible once:
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0030-runtime-mechanics-derived.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ retention can only be an application-level backup job, both falling out of the
durability class of [0015](0015-durability-class-per-volume.md) as probe
timings fall out of [0014](0014-probes-are-siblings.md). The renderer does not
do this yet: `src/adapters/kubernetes.ts:608` reads an authored enum
(`src/schemas/project-intent.ts:165`), inspecting no volume.
(`src/schemas/service-intent.ts:165`), inspecting no volume.

## Alternatives
| option | cost if taken | why rejected |
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/model/0053-adapter-port-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ while `tsconfig.json:11` sets `"strict": true` and `eslint.config.js:33` sets
violated. `grep -rn E_PATH_COLLISION src/` returns nothing, while the writer
applies each prepared file in turn (`src/render-plan/writer.ts:60-66`): two
adapters sharing a path both write, second wins, silently, both reporting
`action: "create"`. `kubernetes-process-fragment` reads raw manifests from disk
inside render (`src/adapters/kubernetes-process-fragment.ts:48-49`, `:236-247`);
`action: "create"`. `kubernetes-workload-fragment` reads raw manifests from disk
inside render (`src/adapters/kubernetes-workload-fragment.ts:48-49`, `:236-247`);
under the port that read moves to the caller, which passes the parsed documents
in, as `loadFragmentInput` already does at `src/adapters/fragment-model.ts:95-98`.
Hence the contract: documents in, attributed Deliverables out, deterministic, no
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0054-adapter-attribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ consumers to shape it.

One owner per path is asserted today and enforced nowhere. Five of the sixteen
registered names end in `-fragment` and shadow an earlier adapter, chapter 30
names four such pairs and misses `kubernetes-process-fragment`; the twins avoid
names four such pairs and misses `kubernetes-workload-fragment`; the twins avoid
a literal collision only because they write under `fragments/` while the first
generation writes under `platform/cluster/flux/`. Three adapters (`kubernetes`,
`flux-packs`, `flux-source`) declare the same `platform/cluster/flux/apps` prefix.
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0073-vault-policy-is-a-deliverable.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ instead. Either way something must exist in Vault that the derivation describes.
The unit is the **Process identity**, not the Application.
[0024](0024-identity-per-process.md) makes the ServiceAccount and the Vault
role the Process name alone, and the estate has already paid for getting this
wrong: `applicationAccountName()` returned the Application name, so two Processes of one
wrong: `serviceAccountName()` returned the Application name, so two Processes of one
Application authenticated as the same principal and received the union of both
policies whatever level a grant was written at. One document per identity is
what makes that impossible to reintroduce, and it makes a diff say which
Expand Down
6 changes: 3 additions & 3 deletions docs/adr/model/0083-privileged-port-needs-the-capability.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ no Process declaring a port below 1024.
## Why
`auth-ui` declared port 80 under the `restricted` class and therefore rendered a
pod that cannot bind its own port: non-root plus `capabilities.drop: [ALL]`
removes `CAP_NET_BIND_APPLICATION`. The render was internally consistent and the
removes `CAP_NET_BIND_SERVICE`. The render was internally consistent and the
process could not start. The model said nothing either way.

Refusing it is the reading that keeps the class honest. Deriving
`NET_BIND_APPLICATION` wherever a low port appears would re-add a dropped capability
`NET_BIND_SERVICE` wherever a low port appears would re-add a dropped capability
for every Process that happens to declare one, silently, and the class would
mean less than it says.

Expand All @@ -53,7 +53,7 @@ during serialisation that the process then contradicts.
## Alternatives
| option | cost if taken | why rejected |
|---|---|---|
| Derive `NET_BIND_APPLICATION` where a low port is declared | Nothing in any project file changes | Silently re-adds a capability the class dropped, for every Process that happens to declare a low port |
| Derive `NET_BIND_SERVICE` where a low port is declared | Nothing in any project file changes | Silently re-adds a capability the class dropped, for every Process that happens to declare a low port |
| Derive an unprivileged `targetPort` | Nothing authored changes and nothing is refused | The process still listens where its image says, so the rendered object and the running pod disagree |
| Leave it to review | No new error code | The render is internally consistent, so review has nothing to notice; the failure appears as a crash-looping pod |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ file, in its own repository. That leaves one real problem, which is the reason
this is a decision rather than a shrug. One of those four lines is
`VAULT_KUBERNETES_ROLE: auth-api`, and the role name is **derived**
([0024](0024-identity-per-process.md)). Written as a literal it is precisely
the staleness that produced the `applicationAccountName()` defect, where a
the staleness that produced the `serviceAccountName()` defect, where a
hand-maintained name and a derived one disagreed and nothing noticed until two
Processes shared a principal.

Hence `${identity:…}`, a fourth named source beside `${dependency:…}`,
`${secret:…}` and `${exposure:…}`. The first three name something else; this one
names what the platform decided about **this** Process (`vaultRole`,
`applicationAccount`, `namespace`), as a closed key set, with no template language,
`serviceAccount`, `namespace`), as a closed key set, with no template language,
exactly like the others. A literal where a placeholder belongs is already a build
error, so the drift closes.

Expand Down Expand Up @@ -83,7 +83,7 @@ though that is a rename, not a redesign.
outcome worth having, paid in four boilerplate lines per self-delivering
Process, in the repository that owns the framework.
- `VAULT_KUBERNETES_ROLE` can no longer disagree with the derived role, so the
`applicationAccountName()` class of defect is closed on the authoring side too,
`serviceAccountName()` class of defect is closed on the authoring side too,
paid by nobody.
- A fourth placeholder source is a fourth thing to validate, complete in an
editor and resolve; the key set is closed so the validation is a lookup, paid
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0098-one-publication-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ byte for byte.
publication one mechanism: everything authored enters composition as an Intent
Fragment by digest. That leaves the five `*-fragment` adapters as the one
remaining second path into the render, and the deletion test decides them.
Remove `traefik-route-fragment`, `kubernetes-process-fragment`,
Remove `traefik-route-fragment`, `kubernetes-workload-fragment`,
`gatus-endpoint-fragment`, `edge-catalog-fragment` and `image-metadata-fragment`
together with `adapter-compat.ts`, and nothing a consumer observes changes: the
central run already derives every one of those kinds from the composed intent.
Expand Down
14 changes: 7 additions & 7 deletions spec/v1/10-project-intent.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,19 +452,19 @@ derived about **this** Process
| key | value |
|---|---|
| `${identity:vaultRole}` | the Process's Vault role, its own name ([0024](../../docs/adr/model/0024-identity-per-process.md)) |
| `${identity:applicationAccount}` | the Process's ServiceAccount name |
| `${identity:serviceAccount}` | the Process's ServiceAccount name |
| `${identity:namespace}` | `<project>-system` |

The key set is closed. It exists because a self-delivering Process has to wire
its own Vault client, and one of the values it wires (the role name) is
derived: written as a literal it is the same staleness class as the
`applicationAccountName()` defect, where a hand-maintained name and a derived one
`serviceAccountName()` defect, where a hand-maintained name and a derived one
disagreed and nothing noticed. Writing a derived value as a literal is a
Writing a derived value as a literal is a build error, and so is writing a Runtime Profile key at all: `OTEL_*` and
`PYROSCOPE_*` come from `runtime`, and an exceptional value is not a layer-1
concept: there is no `overrides` field to put it in. Ten `OTEL_*` variables are
byte-identical today across `auth-api`,
`agents-api` and `knowledge-api` except `OTEL_APPLICATION_NAME`, sixty duplicated
`agents-api` and `knowledge-api` except `OTEL_SERVICE_NAME`, sixty duplicated
lines that leave the project repositories under this rule.

Placeholders are named-source references and never a template language: no
Expand Down Expand Up @@ -547,7 +547,7 @@ fallback** ([0014](../../docs/adr/model/0014-probes-are-siblings.md)). Readiness
*can I serve traffic*; liveness means *is my process wedged*. A liveness probe
pointed at a readiness endpoint turns a dependency outage into a crash-loop, and
the v2 model made that the default for anyone declaring one path:
`src/adapters/kubernetes-process-fragment.ts:166` renders
`src/adapters/kubernetes-workload-fragment.ts:166` renders
`livenessProbe: probe(health.livenessPath ?? health.path)`, and `app-ui` and
`agents-login` both rely on it today.

Expand Down Expand Up @@ -724,7 +724,7 @@ writablePaths: [/var/cache/nginx, /var/run]

The field does not exist today, in either renderer generation:
`grep -rniE 'securityContext|runAsNonRoot|readOnlyRootFilesystem|seccompProfile' src/ schemas/`
returns **0 hits**, and `src/deployment/render/processes.ts:130` builds a
returns **0 hits**, and `src/deployment/render/workloads.ts:130` builds a
container from name, image, pullPolicy, ports, command, args, env, envFrom,
volumeMounts, probes and resources, and stops. Rendered pods run as their image's
UID, with a writable root and default capabilities, and the standing QoS class for
Expand Down Expand Up @@ -807,7 +807,7 @@ nginx writes, and `platform-postgres`, whose UID comes from the lock.
### A privileged port needs the capability that binds it

A `provides` port below 1024 cannot be bound by a non-root process without
`CAP_NET_BIND_APPLICATION`, and the `restricted` class drops all capabilities. A
`CAP_NET_BIND_SERVICE`, and the `restricted` class drops all capabilities. A
Process declaring one is `E_PRIVILEGED_PORT_UNDER_NONROOT`
([0083](../../docs/adr/model/0083-privileged-port-needs-the-capability.md)),
and the answer is a port above 1024. Deriving the capability silently would
Expand Down Expand Up @@ -1428,7 +1428,7 @@ The two levels are an access boundary **only** because identity is per Process.
The ServiceAccount and Vault role are derived as the **Process name alone** (
`auth-system.auth-api`, never `auth-system.auth-auth-api`) unique within the
project file ([0024](../../docs/adr/model/0024-identity-per-process.md), specified in
chapter 16). At review time they were not: `applicationAccountName()` in
chapter 16). At review time they were not: `serviceAccountName()` in
`src/adapters/kubernetes.ts:665-669` returned `applicationName`, so two Processes of
one Application authenticated as the same principal and received the union of both
policies whatever level a grant was written at. The nesting was documentation. The
Expand Down
6 changes: 3 additions & 3 deletions spec/v1/20-resolved-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The estate is the argument for having a rule at all. One hostname,
`kb.jorisjonkers.dev`, ended up declared in seven authoritative places across
three repositories (`homelab-inventory/catalog/reachability.yml`, three
`fleet-infra` manifests, a bearer-token secret and the application's own
`platform/deployment.yml`) plus hardcoded in `ApplicationPermission.kt`, with two
`platform/deployment.yml`) plus hardcoded in `ServicePermission.kt`, with two
conformance tests existing for no purpose but detecting when the seven disagree.
The guard was cheaper to write than the fix.

Expand Down Expand Up @@ -824,7 +824,7 @@ assigned:

processes:
knowledge-api:
applicationAccount: knowledge-api # the Process name alone
serviceAccount: knowledge-api # the Process name alone
objectKind: Deployment
image: ghcr.io/jorisjonkers-dev/knowledge/knowledge-api@sha256:1ad39d5…
probes:
Expand All @@ -850,7 +850,7 @@ assigned:
- {kind: VaultStaticSecret, path: secret/data/platform/postgres/kb}

knowledge-ingest-worker:
applicationAccount: knowledge-ingest-worker
serviceAccount: knowledge-ingest-worker
objectKind: Deployment
strategy: {type: Recreate} # forced: RWO volume
resources:
Expand Down
2 changes: 1 addition & 1 deletion spec/v1/30-deliverables.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ in Project Intent implies a dashboard's panels; deriving one would be inventing
dashboard DSL. It is ledgered rather than permanent: 14 application dashboards become
Assets on the owning Application ([0012](../../docs/adr/model/0012-assets-not-code.md)), 3
runtime-family dashboards ship with the Runtime Profile, 14 platform dashboards
become Assets of the declared observability Applications, and `application-overview` / `application-template` derive
become Assets of the declared observability Applications, and `service-overview` / `service-template` derive
per Application from the scrape surface and exposure
([0021](../../docs/adr/model/0021-observability-scrape-and-alert-class.md)).

Expand Down
Loading
Loading