Spec: catalog-driven app bootstrapping and capability groups #2

Open
opened 2026-08-11 15:11:31 +00:00 by ginjiruu · 0 comments
Owner

Spec: catalog-driven app bootstrapping and capability groups

Problem Statement

The iac repo has no single entrypoint for "an app exists". App resources are
added ad hoc across vault/, kanidm/, and separate repos, and a previous
attempt left placeholder modules (forgejo/k8s/kanidm/vault) that are
unplannable: the forgejo root fails provider resolution and the modules
reference providers that don't exist. Meanwhile there is no declarative way to
give developers per-app access across Forgejo, Argo CD, Kubernetes, and Vault —
access is hand-granted against the ~90 per-app UI groups.

Solution

Make apps/catalog.yaml the single bootstrap entrypoint for an app: one entry
declares the platform mechanics (namespace, service accounts, Forgejo org and
repos, Argo app-any-namespace resources, Vault roles) and the developer-facing
capability groups for the app. Provisioning runs as the platform-operator
class (elevated credentials); app principals hold per-app capability groups,
never cluster-wide power. The model is specified in ADR-0006 (catalog-driven
capability model) and ADR-0007 (per-system permission matrix).

User Stories

  1. As a platform operator, I want to add one catalog entry per app, so that the
    namespace, service accounts, Forgejo org/repos, Argo resources, and Vault
    roles are all bootstrapped from a single source.
  2. As a platform operator, I want the catalog to be the only place an app's
    platform mechanics are declared, so that no resource is hand-created ad hoc.
  3. As a platform operator, I want provisioning to run with elevated
    platform-operator credentials, so that app principals never receive
    cluster-wide power.
  4. As an app developer, I want per-app capability groups that grant access
    across Forgejo, Argo CD, Kubernetes, and Vault, so that one membership gives
    me the right level of access everywhere.
  5. As an app developer, I want the capability groups to be strictly
    independent, so that granting more access means more memberships, never
    inheritance that over-grants.
  6. As an app developer, I want capability-first group names (viewer,
    contributor, developer, maintainer, live-debugging,
    generated-secret-manager, manual-secret-manager, k8s-secret-viewer), so
    that the name says what the group does.
  7. As an app developer, I want viewer to let me view code/issues, pipelines,
    namespaced resources (not secrets), metrics/logs, and the Argo app.
  8. As an app developer, I want contributor to let me create branches and pull
    requests in Forgejo.
  9. As an app developer, I want developer to let me merge PRs, run workflows,
    create namespaced resources (except secrets), and sync the Argo app.
  10. As an app developer, I want maintainer to let me commit to protected
    branches (Forgejo Admin level).
  11. As an app developer, I want live-debugging to let me exec/attach/
    port-forward into the app's pods and use the Argo CD web terminal.
  12. As an app developer, I want generated-secret-manager to let me full-CRUD
    the app's generated secrets in Vault.
  13. As an app developer, I want manual-secret-manager to let me full-CRUD the
    app's manual secrets in Vault, without seeing generated secrets.
  14. As an app developer, I want k8s-secret-viewer to let me view Secrets in
    the app namespace (all-or-nothing), distinct from Vault secret access.
  15. As a platform operator, I want <app>-root to be a nested group that is a
    member of all capability groups, so that granting root grants everything
    for the app, transitively.
  16. As a platform operator, I want root to grant no permissions the other
    groups don't grant, so that its tightness is membership discipline, not
    capability.
  17. As a platform operator, I want root to be namespace-scoped and grant
    nothing outside the app, so that an app group can never reach cluster
    resources.
  18. As a platform operator, I want a machine to hold the same groups as a
    human, so that service accounts and CI can be granted the same per-app
    access (no separate machine tier).
  19. As a platform operator, I want the forgejo provider root to validate and
    plan, so that the Forgejo org/repo module can actually run.
  20. As a platform operator, I want the k8s and kanidm modules to resolve their
    providers, so that all roots plan cleanly.
  21. As a platform operator, I want the catalog to drive namespace creation for
    each app (via the k8s module), so that apps land in their own namespace.
  22. As a platform operator, I want the catalog to drive Forgejo org and repo
    creation, so that each app gets its org and repos without manual setup.
  23. As a platform operator, I want Argo CD RBAC scoped per AppProject, so that
    developer/maintainer can sync/update only their own app.
  24. As a platform operator, I want k8s Roles scoped to the app namespace, so
    that capability groups map to namespace-scoped RBAC.
  25. As an app developer, I want to see app metrics/logs in Victoria/Grafana, so
    that viewer includes observability access.
  26. As a platform operator, I want the existing ~90 per-app UI groups left
    untouched, so that the new model is additive and non-disruptive.
  27. As a platform operator, I want secret values still written only by ESO/VSO
    and human manual writes (ADR-0002), so that the capability groups grant
    access, not value-writing.
  28. As a platform operator, I want the forgejo provider at a version that
    supports forgejo_team, so that the per-app team/group mapping can be
    expressed.

Implementation Decisions

  • Catalog shape: apps/catalog.yaml becomes the single source of truth,
    per app: namespace, service accounts, Forgejo org + repos, Argo
    app-any-namespace resources, Vault roles, and the list of activated
    capability groups. The catalog drives all modules; each system root reads it
    and transforms to its module input shape.
  • Capability groups (ADR-0006, ADR-0007): viewer, contributor, developer,
    maintainer, live-debugging, generated-secret-manager, manual-secret-manager,
    k8s-secret-viewer, root. Strictly independent; progress via more memberships.
    <app>-root is a nested group (member of all capability groups; grants
    nothing beyond their union; namespace-scoped; no provisioning powers).
  • Actors: platform-operator (elevated, runs the IaC) vs app principals
    (humans or machines holding capability groups). No machine tier.
  • Per-system matrix (ADR-0007):
    • k8s: namespace Role per group (viewer get/list non-secret; contributor
      read; developer edit non-secret; maintainer edit+write; live-debugging
      get pods + create on pods/exec, pods/attach, pods/portforward;
      k8s-secret-viewer get/list secrets; root Role * incl secrets). No
      ClusterRole — StorageClasses/Gateways/IngressClasses are cluster-operator
      resources.
    • Forgejo: per-unit team levels (viewer Read all; contributor Write
      Code/Issues/PRs; developer Write Code/PRs/Actions/Releases; maintainer
      Admin; root Owner). Requires forgejo_team (1.x provider).
    • Argo CD: AppProject roles (viewer get + logs; contributor get + logs;
      developer get + sync; maintainer get + sync + update; root project admin).
    • Vault: generated-secret-manager full CRUD on generated/<app>;
      manual-secret-manager full CRUD on manual/<app>; root both classes.
  • Forgejo provider fixes: root must declare svalabs/forgejo with
    host/api_token (not base_url/token), version ~> 1.0 to get
    forgejo_team; the module needs its own required_providers so the local
    name resolves to svalabs/forgejo (it currently resolves to
    hashicorp/forgejo).
  • k8s provider: hashicorp/kubernetes ~> 2.33 resolves to 2.38.0 and
    plans cleanly; keep. The module needs its own required_providers for the
    local name.
  • kanidm provider: seanlatimer/kanidm ~> 0.1.10 resolves to 0.1.10; the
    dev_groups module needs required_providers so the local name resolves
    (currently the module references kanidm but doesn't declare it).
  • Vault: the placeholder app_secrets module's k8s-auth variables are
    dropped (contradicts ADR-0004 VSO read path); per-app identity groups +
    aliases on both mounts (oidc, oidc-cli) per the one-alias-per-group
    constraint, mapping capability groups to Vault policies.
  • Namespace/service-account ownership: cluster operators / the terraform
    provider create namespaces; the Argo Application lives in the app's own git
    repo (app-any-namespace).
  • Deferred: portal API, manifest-generator Argo workflow → PR to catalog.

Testing Decisions

  • Test external behavior, not implementation: tofu validate / tofu plan per
    root is the primary seam (a root that doesn't plan fails the test).
  • Module interfaces are tested by running tofu validate against a
    representative catalog (e.g. one small app) and asserting the planned
    resources match the ADR-0007 matrix (Role rules, Forgejo team levels, Vault
    identity groups, Argo roles).
  • Prior art: the existing vault/ root already plans against real state; the
    kanidm/vault roots have lock files and state. New tests are validate/plan
    assertions, not unit tests (this is IaC, not an application).
  • Provider resolution is tested by tofu init succeeding per root (catches the
    hashicorp/svalabs namespace mismatch).

Out of Scope

  • The portal API / internal developer portal frontend (deferred decision #4).
  • The manifest-generator Argo Workflow that PRs to the catalog (deferred).
  • The k8s-apiserver OIDC flags and flux-clusters-side Argo AppProject RBAC
    (separate flux-clusters tickets; this spec scopes the iac-repo side).
  • The existing ~90 per-app UI groups — untouched, additive only.
  • Secret value writing — ADR-0002 ownership unchanged.
  • Migration of existing apps into the catalog (pilot: privatebin, syncthing).
  • Vault kubernetes-auth backend for human access (VSO is the read path,
    ADR-0004).

Further Notes

  • Two hard defects from the prior attempt are in-scope: the forgejo provider
    arg mismatch (base_url/tokenhost/api_token) and the module-level
    provider-name mismatch (module must declare svalabs/forgejo, currently
    resolves to hashicorp/forgejo).
  • forgejo_team requires provider ≥1.x; pinning ~> 0.2 cannot express the
    per-app team/group mapping.
  • References: ADR-0006, ADR-0007, CONTEXT.md ("App catalog and capability
    groups"), plans/per-app-policy-rollout.md, ADR-0002/0004.
# Spec: catalog-driven app bootstrapping and capability groups ## Problem Statement The iac repo has no single entrypoint for "an app exists". App resources are added ad hoc across `vault/`, `kanidm/`, and separate repos, and a previous attempt left placeholder modules (forgejo/k8s/kanidm/vault) that are unplannable: the forgejo root fails provider resolution and the modules reference providers that don't exist. Meanwhile there is no declarative way to give developers per-app access across Forgejo, Argo CD, Kubernetes, and Vault — access is hand-granted against the ~90 per-app UI groups. ## Solution Make `apps/catalog.yaml` the single bootstrap entrypoint for an app: one entry declares the platform mechanics (namespace, service accounts, Forgejo org and repos, Argo app-any-namespace resources, Vault roles) **and** the developer-facing capability groups for the app. Provisioning runs as the `platform-operator` class (elevated credentials); app principals hold per-app capability groups, never cluster-wide power. The model is specified in ADR-0006 (catalog-driven capability model) and ADR-0007 (per-system permission matrix). ## User Stories 1. As a platform operator, I want to add one catalog entry per app, so that the namespace, service accounts, Forgejo org/repos, Argo resources, and Vault roles are all bootstrapped from a single source. 2. As a platform operator, I want the catalog to be the only place an app's platform mechanics are declared, so that no resource is hand-created ad hoc. 3. As a platform operator, I want provisioning to run with elevated `platform-operator` credentials, so that app principals never receive cluster-wide power. 4. As an app developer, I want per-app capability groups that grant access across Forgejo, Argo CD, Kubernetes, and Vault, so that one membership gives me the right level of access everywhere. 5. As an app developer, I want the capability groups to be strictly independent, so that granting more access means more memberships, never inheritance that over-grants. 6. As an app developer, I want capability-first group names (viewer, contributor, developer, maintainer, live-debugging, generated-secret-manager, manual-secret-manager, k8s-secret-viewer), so that the name says what the group does. 7. As an app developer, I want `viewer` to let me view code/issues, pipelines, namespaced resources (not secrets), metrics/logs, and the Argo app. 8. As an app developer, I want `contributor` to let me create branches and pull requests in Forgejo. 9. As an app developer, I want `developer` to let me merge PRs, run workflows, create namespaced resources (except secrets), and sync the Argo app. 10. As an app developer, I want `maintainer` to let me commit to protected branches (Forgejo Admin level). 11. As an app developer, I want `live-debugging` to let me exec/attach/ port-forward into the app's pods and use the Argo CD web terminal. 12. As an app developer, I want `generated-secret-manager` to let me full-CRUD the app's generated secrets in Vault. 13. As an app developer, I want `manual-secret-manager` to let me full-CRUD the app's manual secrets in Vault, without seeing generated secrets. 14. As an app developer, I want `k8s-secret-viewer` to let me view Secrets in the app namespace (all-or-nothing), distinct from Vault secret access. 15. As a platform operator, I want `<app>-root` to be a nested group that is a member of all capability groups, so that granting root grants everything for the app, transitively. 16. As a platform operator, I want root to grant no permissions the other groups don't grant, so that its tightness is membership discipline, not capability. 17. As a platform operator, I want root to be namespace-scoped and grant nothing outside the app, so that an app group can never reach cluster resources. 18. As a platform operator, I want a machine to hold the same groups as a human, so that service accounts and CI can be granted the same per-app access (no separate machine tier). 19. As a platform operator, I want the forgejo provider root to validate and plan, so that the Forgejo org/repo module can actually run. 20. As a platform operator, I want the k8s and kanidm modules to resolve their providers, so that all roots plan cleanly. 21. As a platform operator, I want the catalog to drive namespace creation for each app (via the k8s module), so that apps land in their own namespace. 22. As a platform operator, I want the catalog to drive Forgejo org and repo creation, so that each app gets its org and repos without manual setup. 23. As a platform operator, I want Argo CD RBAC scoped per AppProject, so that developer/maintainer can sync/update only their own app. 24. As a platform operator, I want k8s Roles scoped to the app namespace, so that capability groups map to namespace-scoped RBAC. 25. As an app developer, I want to see app metrics/logs in Victoria/Grafana, so that `viewer` includes observability access. 26. As a platform operator, I want the existing ~90 per-app UI groups left untouched, so that the new model is additive and non-disruptive. 27. As a platform operator, I want secret values still written only by ESO/VSO and human manual writes (ADR-0002), so that the capability groups grant access, not value-writing. 28. As a platform operator, I want the forgejo provider at a version that supports `forgejo_team`, so that the per-app team/group mapping can be expressed. ## Implementation Decisions - **Catalog shape**: `apps/catalog.yaml` becomes the single source of truth, per app: namespace, service accounts, Forgejo org + repos, Argo app-any-namespace resources, Vault roles, and the list of activated capability groups. The catalog drives all modules; each system root reads it and transforms to its module input shape. - **Capability groups** (ADR-0006, ADR-0007): viewer, contributor, developer, maintainer, live-debugging, generated-secret-manager, manual-secret-manager, k8s-secret-viewer, root. Strictly independent; progress via more memberships. `<app>-root` is a nested group (member of all capability groups; grants nothing beyond their union; namespace-scoped; no provisioning powers). - **Actors**: `platform-operator` (elevated, runs the IaC) vs app principals (humans or machines holding capability groups). No machine tier. - **Per-system matrix** (ADR-0007): - k8s: namespace Role per group (viewer get/list non-secret; contributor read; developer edit non-secret; maintainer edit+write; live-debugging get pods + create on pods/exec, pods/attach, pods/portforward; k8s-secret-viewer get/list secrets; root Role `*` incl secrets). No ClusterRole — StorageClasses/Gateways/IngressClasses are cluster-operator resources. - Forgejo: per-unit team levels (viewer Read all; contributor Write Code/Issues/PRs; developer Write Code/PRs/Actions/Releases; maintainer Admin; root Owner). Requires `forgejo_team` (1.x provider). - Argo CD: AppProject roles (viewer get + logs; contributor get + logs; developer get + sync; maintainer get + sync + update; root project admin). - Vault: generated-secret-manager full CRUD on `generated/<app>`; manual-secret-manager full CRUD on `manual/<app>`; root both classes. - **Forgejo provider fixes**: root must declare `svalabs/forgejo` with `host`/`api_token` (not `base_url`/`token`), version `~> 1.0` to get `forgejo_team`; the module needs its own `required_providers` so the local name resolves to `svalabs/forgejo` (it currently resolves to `hashicorp/forgejo`). - **k8s provider**: `hashicorp/kubernetes ~> 2.33` resolves to 2.38.0 and plans cleanly; keep. The module needs its own `required_providers` for the local name. - **kanidm provider**: `seanlatimer/kanidm ~> 0.1.10` resolves to 0.1.10; the dev_groups module needs `required_providers` so the local name resolves (currently the module references `kanidm` but doesn't declare it). - **Vault**: the placeholder `app_secrets` module's k8s-auth variables are dropped (contradicts ADR-0004 VSO read path); per-app identity groups + aliases on both mounts (oidc, oidc-cli) per the one-alias-per-group constraint, mapping capability groups to Vault policies. - **Namespace/service-account ownership**: cluster operators / the terraform provider create namespaces; the Argo Application lives in the app's own git repo (app-any-namespace). - **Deferred**: portal API, manifest-generator Argo workflow → PR to catalog. ## Testing Decisions - Test external behavior, not implementation: `tofu validate` / `tofu plan` per root is the primary seam (a root that doesn't plan fails the test). - Module interfaces are tested by running `tofu validate` against a representative catalog (e.g. one small app) and asserting the planned resources match the ADR-0007 matrix (Role rules, Forgejo team levels, Vault identity groups, Argo roles). - Prior art: the existing `vault/` root already plans against real state; the kanidm/vault roots have lock files and state. New tests are validate/plan assertions, not unit tests (this is IaC, not an application). - Provider resolution is tested by `tofu init` succeeding per root (catches the hashicorp/svalabs namespace mismatch). ## Out of Scope - The portal API / internal developer portal frontend (deferred decision #4). - The manifest-generator Argo Workflow that PRs to the catalog (deferred). - The k8s-apiserver OIDC flags and flux-clusters-side Argo AppProject RBAC (separate flux-clusters tickets; this spec scopes the iac-repo side). - The existing ~90 per-app UI groups — untouched, additive only. - Secret value writing — ADR-0002 ownership unchanged. - Migration of existing apps into the catalog (pilot: privatebin, syncthing). - Vault kubernetes-auth backend for human access (VSO is the read path, ADR-0004). ## Further Notes - Two hard defects from the prior attempt are in-scope: the forgejo provider arg mismatch (`base_url`/`token` → `host`/`api_token`) and the module-level provider-name mismatch (module must declare `svalabs/forgejo`, currently resolves to `hashicorp/forgejo`). - `forgejo_team` requires provider ≥1.x; pinning `~> 0.2` cannot express the per-app team/group mapping. - References: ADR-0006, ADR-0007, CONTEXT.md ("App catalog and capability groups"), plans/per-app-policy-rollout.md, ADR-0002/0004.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lab/iac#2
No description provided.