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:
- Given a deployment whose roster holds the
garagenode, When the setups evaluate, Then the node importsgarage.mainConfig,garage.layoutBootstrapand theprovisionFormodule of its own group's garage requests. - Given a garage request with key files, When the node renders, Then
openbao-agentwrites each key file fromsecret/data/<path>with mode0640. - Given the
webrequest, When the firewall resource applies it, Then TCP ports80and443are 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:
- Given the policy sets
deployAdminUIs, When theoidcresource applies thegarage-uirequest, Then the vhostgarage-webui.<apex>asks forone_factorand proxies to127.0.0.1:3909. - Given the policy does not set
deployAdminUIs, When theoidcresource applies the request, Then it emits no vhost and the setups count no served name for it. - Given a rotated admin token, When
openbao-agentwrites it again, Thenvars-openbaorestartsgarage-webui.serviceand the UI loads the new token.
Edge Cases
LoadCredentialcopies the admin token only at start. A new token reaches the UI only through the restart inservices.vars-openbao.reloadUnits.- The
garage-uirequest setsallowedGroups = [ ]. Every user who passesone_factorgets the UI, unlike thewindmillUI. garage-webuistarts aftergarage.servicewithWants=. If Garage fails, the UI restarts withRestart=on-failure.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
components.garagewithpackage = pkgs.garage, the Garage icons and thesharedOptions.nixoptions. - FR-002: The implementation MUST request
spiffe.enableunder the namespiffe. - FR-003: The implementation MUST request
firewall.publicTCPPorts = [ 80 443 ]under the nameweb, because clients on the internet fetch media from the buckets. - FR-004: The implementation MUST request
oidc.forwardAuthunder the namegarage-uiwithdomain = "garage-webui.<apex>",upstream = "127.0.0.1:3909",authorizationPolicy = "one_factor"andallowedGroups = [ ]. The request carries no own condition, becausedeployAdminUIsgates it in the resource. - FR-005: The component MUST import
garage-webui.nixthrough itsnixos-modulerequest.garage-webui.nixMUST set thegarage-webuiunit only whenfediversity.forwardAuth.domainshas an entry for127.0.0.1:3909, the upstream of thegarage-uirequest. - FR-006:
garage-webui.nixMUST runsystemd.services.garage-webuiwithDynamicUser,PORT=3909,API_BASE_URL=http://127.0.0.1:3903,S3_ENDPOINT_URL=http://127.0.0.1:3900andS3_REGION=garage. - FR-007:
garage-webui.nixMUST load the admin token fromcontracts.generateFiles.results."garage-admin".files.adminToken.pathwithLoadCredential, and export it asAPI_ADMIN_KEY. - FR-008:
garage-webui.nixMUST addgarage-webui.servicetoservices.vars-openbao.reloadUnitsof 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 theobjectStoreprovider node, not to a node by name.
Key Entities
garage-uirequest: the forward-auth vhost of the web UI.garage-admingenerated file: the admin token. Bothgarageandgarage-webuiread it.- Garage request: a request of the
garageresource. 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-reachandnix-unit-setups-wildcard-tlspass. - SC-002:
nix-unit-setups-vars-reloadandnix-unit-setups-apex-cohesionpass. - 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/adminTokenon the Garage node. - The entry point passes the apex as the
apexmodule argument.core/apex.nixreads it fromDNS_DOMAIN.
Known Deviations
None.