Feature Specification: Component garage

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/components/hosting-provider/garage/default.nix, core/components/hosting-provider/garage/garage-webui.nix, core/setups/hosts-common.nix, core/resources/oidc/forward-auth-domain.nix, core/setups/vars-reload-test.nix, core/checks/nix-unit-tests.nix).

User Scenarios & Testing (mandatory)

User Story 1 - Run the shared S3 store of the hosting provider (Priority: P1)

The hosting provider needs one S3 store for all tenants. The garage component marks the node of the hosting-provider group that runs Garage. The component itself carries no daemon config. The setups add the server config, the layout bootstrap and the bucket provisioning to this node (see garage and 005). Buckets and keys carry the prefix <tenant>-<name>.

Why this priority: operator backups and the telemetry stores keep their data in these buckets. Without the node, no bucket exists.

Independent Test: nix-unit-setups-minted-garage-keys holds that each bucket the provisioner creates has a key, and that the key pair renders where the import reads it. nix-unit-setups-internal-reach holds that the v2 admin API is served but not published. No NixOS test boots this component as a whole.

Acceptance Scenarios:

  1. Given a deployment whose roster holds the garage node, When the setups evaluate, Then the node imports garage.mainConfig, garage.layoutBootstrap and the provisionFor module of its own group's garage requests.
  2. Given a garage request with key files, When the node renders, Then openbao-agent writes each key file from secret/data/<path> with mode 0640.
  3. Given the web request, When the firewall resource applies it, Then TCP ports 80 and 443 are open to the internet.

User Story 2 - Browse buckets in a web UI (Priority: P2)

An administrator wants to look at the buckets and keys without the CLI. The component requests a forward-auth vhost garage-webui.<apex> in front of garage-webui on 127.0.0.1:3909. Authelia checks the session first.

Why this priority: it is a convenience for operations. Garage works without it.

Independent Test: nix-unit-setups-vars-reload holds that a new render of /run/vars/secret/garage-admin/adminToken restarts both garage and garage-webui. nix-unit-setups-apex-cohesion holds that the UI name is a label under the apex. No test opens the UI in a browser.

Acceptance Scenarios:

  1. Given the policy sets deployAdminUIs, When the oidc resource applies the garage-ui request, Then the vhost garage-webui.<apex> asks for one_factor and proxies to 127.0.0.1:3909.
  2. Given the policy does not set deployAdminUIs, When the oidc resource applies the request, Then it emits no vhost and the setups count no served name for it.
  3. Given a rotated admin token, When openbao-agent writes it again, Then vars-openbao restarts garage-webui.service and the UI loads the new token.

Edge Cases

  • LoadCredential copies the admin token only at start. A new token reaches the UI only through the restart in services.vars-openbao.reloadUnits.
  • The garage-ui request sets allowedGroups = [ ]. Every user who passes one_factor gets the UI, unlike the windmill UI.
  • garage-webui starts after garage.service with Wants=. If Garage fails, the UI restarts with Restart=on-failure.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare components.garage with package = pkgs.garage, the Garage icons and the sharedOptions.nix options.
  • FR-002: The implementation MUST request spiffe.enable under the name spiffe.
  • FR-003: The implementation MUST request firewall.publicTCPPorts = [ 80 443 ] under the name web, because clients on the internet fetch media from the buckets.
  • FR-004: The implementation MUST request oidc.forwardAuth under the name garage-ui with domain = "garage-webui.<apex>", upstream = "127.0.0.1:3909", authorizationPolicy = "one_factor" and allowedGroups = [ ]. The request carries no own condition, because deployAdminUIs gates it in the resource.
  • FR-005: The component MUST import garage-webui.nix through its nixos-module request. garage-webui.nix MUST set the garage-webui unit only when fediversity.forwardAuth.domains has an entry for 127.0.0.1:3909, the upstream of the garage-ui request.
  • FR-006: garage-webui.nix MUST run systemd.services.garage-webui with DynamicUser, PORT=3909, API_BASE_URL=http://127.0.0.1:3903, S3_ENDPOINT_URL=http://127.0.0.1:3900 and S3_REGION=garage.
  • FR-007: garage-webui.nix MUST load the admin token from contracts.generateFiles.results."garage-admin".files.adminToken.path with LoadCredential, and export it as API_ADMIN_KEY.
  • FR-008: garage-webui.nix MUST add garage-webui.service to services.vars-openbao.reloadUnits of the admin token file.
  • FR-009: The component MUST declare provides = [ "objectStore" ]. The setups MUST give the Garage server config, the layout bootstrap, the bucket provisioning, the S3 key generators and the aggregated TLS names to the objectStore provider node, not to a node by name.

Key Entities

  • garage-ui request: the forward-auth vhost of the web UI.
  • garage-admin generated file: the admin token. Both garage and garage-webui read it.
  • Garage request: a request of the garage resource. Its buckets and keys get the tenant prefix.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-setups-minted-garage-keys, nix-unit-setups-garage-kv-stems, nix-unit-setups-internal-reach and nix-unit-setups-wildcard-tls pass.
  • SC-002: nix-unit-setups-vars-reload and nix-unit-setups-apex-cohesion pass.
  • SC-003: On a live node, https://garage-webui.<apex>/ sends a browser without a session to the Authelia portal.

Assumptions

  • Authelia runs in the same deployment and serves the forward-auth endpoint (see authelia).
  • OpenBao renders garage-admin/adminToken on the Garage node.
  • The entry point passes the apex as the apex module argument. core/apex.nix reads it from DNS_DOMAIN.

Known Deviations

None.