reproducible project infrastructure #336
Labels
No labels
0 points
0.5 points
1 point
13 points
2 points
21 points
3 points
34 points
5 points
55 points
8 points
ambition
application-offering
ambition
configure-applications
ambition
front-end
ambition/install-applications
ambition
security
ambition
switch-host
ambition
update-applications
ambition
user-management
api service
blocked
component: fediversity panel
component: nixops4
documentation
estimation high: >3d
estimation low: <2h
estimation mid: <8h
infinite points
productisation
project-management
question
role: application developer
role: application operator
role: hosting provider
role: maintainer
security
technical debt
testing
type unclear
type: bug
type: deliverable
type: key result
type: objective
type: task
type: user story
user experience
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
Depends on
#288 external developers empowered to contribute
fediversity/fediversity
#309 port infra to terraform
fediversity/fediversity
#325 Reproducible proxmox installation
fediversity/fediversity
#356 reproduce CI runner
fediversity/fediversity
#370 fediversity apps reused in infra
fediversity/fediversity
#433 deployment provisions host infra
fediversity/fediversity
#833 unit-tested functionality integrated in deployment pipelines
fediversity/fediversity
Reference
fediversity/fediversity#336
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
As a developer,
I want to be able to reproduce relevant project infrastructure,
so that I may test the project and independently work on the project.
integrations-octodnsapps-tf-incustf-incusk8s (#598)deployment order of core components should probably be (see #882):
(if needed earlier maybe transfer from a temp back-end like TF cloud, otherwise seed by non-TF deployment method, e.g. ssh/nixos-infect/usb / manual install instructions, maybe parameterized using cli params or- standard TF bootstrap pattern: local-then-migrate (state migration):examples/hosting-env-config.nix- could be updated by TF-based deployment methods after if needed)once this bootstrap has resolved, it can restore (for this set of machines) missing/seeded functionality like:
these so far both derive from openbao.
dependencies
multi-tenancy
Can one shared instance serve multiple isolated tenants?
update-policy zonesub, ACLs, catalog zonesauth_map/storage_mapClean split:
Key caveats to know: OpenBao shares one seal/storage today (single unseal exposes all tenants); BIND isolates the zone not the process; restic storage-layer isolation needs
rest-server --private-repos --append-only.On the garage/valkey-style constructions
I documented the reusable minting pattern in the plan. One honest correction to the framing: on this branch, garage per-app minting runs as an
ensure-garagesystemd unit on the garage node (resourcerequest→ aggregate →applyemitsgarageSide), not an operator-TF graph. The "mint per-tenant in the operator-TF graph" model (so a shared HP node isn't redeployed on operator change) is the target from sibling branches (central-valkey,host-garage-central) —valkeydoesn't even exist as a resource here yet. A real shared-HP rollout should adopt that operator-TF variant: same request/applyshape, butapplyemits OpenTofu resources (garage TF provider / OpenBao namespace / BIND zone+TSIG) keyed per tenant.This also points to introducing an explicit
tenantentity insrc/lib/data-model.nixso per-tenant-vs-shared routing becomes first-class (today it's implicit in operator == tenant).Want me to go deeper on any one service's concrete implementation (e.g. sketch the OpenBao-namespace or BIND-zone resource against the data-model), or draft the
tenantentity addition?Multi-tenancy assessment: shared hosting-provider services
Context
The setup splits work into two deployment groups (
src/setups/groups/):operator.nix) — single-tenant applications (mastodon/peertube/pixelfed ontest04/05/06) plus per-deployment ancilliary provider nodes (garage,authelia,smtp/maddy,lldap). One operator deployment == effectively one tenant today.hosting-provider.nix) — shared infrastructure meant to serve many operators:panel,api,incus,terraform-backend,netbox,openbao,windmill,spire,otel-collector,monitoring, plusauthelia+lldapas the admin-UI IdP.Today there is no explicit
tenantentity insrc/lib/data-model.nix. Shared services namespace per-application (per-app garage buckets/keys, per-app lldap groups, per-app authelia OIDC clients, per-app smtp accounts, per-consumer secret key prefixes) — not per-tenant. The existing per-app minting precedent is garage (see the reference-pattern section below); operator == tenant today, so per-app == per-tenant.We want to expand the hosting-provider offering. This document answers, per service, whether one shared instance can safely serve multiple isolated tenants, distinguishing enforced isolation from soft/config-only separation, and — for services that cannot — points the deployment at the existing one-instance-per-tenant pattern in the operator group.
This is an assessment / decision document, not a code change. No files are edited as part of this plan; the "next step" sub-sections describe where implementation would land if/when we act on a verdict.
Per-service verdicts
Legend: Shared-safe = one instance, many tenants, enforced isolation. Shared-soft = one instance can serve many tenants but separation is config-driven, not enforced. Per-tenant = must run one instance per tenant.
OpenBao — Shared-safe (strongest)
src/applications/hosting-provider/openbao/); consumed viavars-openbaocontract +sslopenbao-pkiprovider. Today flat key prefixes, no namespaces.vars/PKI paths under it; provider keeps the root namespace.PostgreSQL — Shared-safe
CONNECT/publicfrom other tenants' DBs). Strongest of the three PG models (vs schema-per-tenant = logical only; vs RLS = weakest physical separation).max_connections; migrations fan out per DB.services.postgresql); no shared cluster, no fediversity resource wrapping it.BIND DNS — Shared-safe (by design)
namedserves thousands of zones with enforced per-tenant write authority: per-tenant TSIG key +update-policy grant <tenantkey> zonesub(orsubdomain) binds a key to exactly that tenant's zone/namespace slice;allow-transfer/allow-queryACLs stop cross-tenant reads (AXFR); catalog zones automate onboarding/offboarding across the primary/secondary fleet.named, no per-tenant resource isolation); per-tenant distinct TSIG keys are mandatory (reuse collapses the boundary); lock downallow-transferdefaults.update-policy zonesubper tenant, optional catalog zone for fleet scale.maddy (SMTP) — Shared-soft
auth_map/storage_mapcollapses it.src/resources/smtp/maddy-provider.nix); one shared relay with per-app submission accounts (<app>@<domain>+ KV-stored password).lldap — Per-tenant (cannot share)
ou=people/ou=groups, global permissions. Only soft workaround = fully-qualified-email usernames; no per-tenant ACLs.src/resources/ldap/lldap-provider.nix), plus one in hosting-provider for the admin UIs. Per-app groups exist but that is app-scoping, not tenant-scoping.Authelia — Per-tenant (cannot share for end-users)
access_control.rules, regex/capture groups from #1339) over a shared user pool — logical filtering, not isolation. Sibling subdomains of one root can't be separate tenants (shared cookie domain); multi-root-domain support exists but with no inter-domain SSO. Maintainers decline to build true tenancy and recommend one instance per org.src/resources/sso/authelia-sso-provider.nix), LDAP-backed viaauthelia-ldap-backend.nix. Per-app OIDC clients = app-scoping, not tenant-scoping.restic (backup) — Per-tenant repository (cannot share one repo)
rest-server --private-repos(each authenticated user confined to/<user>/subtree) +--append-only(block deletion of existing backups). Server-wide flags, not per-tenant toggles (#74).streaming-backupcontract (nix/contracts/nixos/modules/services/backup/restic.nix), backing PG + redis dumps to S3 (garage). Already effectively one-repo-per-(app), and thus per-tenant since operator == tenant.rest-server --private-repos --append-onlyand mint a per-tenant repo path + credential + restic password in the operator group (operator-TF graph).Summary table
update-policy zonesub, ACLs, catalog zonesauth_map/storage_map--private-reposReference pattern: how garage mints per-app (per-tenant) slices
This is the reusable construction the per-tenant services above should mirror. The driving model lives in
src/lib/data-model.nix(resource =request+policy, lines ~62-125): an app declares a request, the policy's genericprocesscollects all apps' requests of that type into one list, and the policy'sapplyturns that aggregated list into node configs.garage (
src/resources/garage/default.nix) is the worked example:src/applications/operator/mastodon/default.nix):ensureBuckets.mastodon,ensureBuckets.mastodon-backups,ensureKeys.mastodonwith per-bucketensureAccess(read/write/owner), plus anixos-configurationthat wires the app's S3 endpoint.resource.policy.process(data-model.nix) filters every app's requests down to the garage ones and flattens them to a single list — collected in the setup atsrc/setups/hosts-common.nix(theancilliaryRequests/processwiring, ~lines 302-328, 649).garage/default.nix'sapply(requests)emits three things from the aggregated list:applicationSide— per-app NixOS config (S3 endpoint, bucket domain, key file paths) wired into each consuming app node.garageSide— aensure-garagesystemd oneshot on the garage node that, aftergarage.service, replays the aggregated requests:garage bucket create/key import/bucket allowfor every app's buckets+keys in one pass.sslDomains— cross-node TLS domains to register.garageSideonto the garage node andapplicationSideonto each app node (hosts-common.nix~lines 869-873, 947-952).Net shape: per-app request -> generic aggregation -> single
apply-> (per-app config wired to app nodes) + (one provisioning unit on the shared node that mints all slices) + (cross-node artifacts). Isolation is by bucket name + per-app key with scoped grants.Important caveat about where minting runs. In this branch garage provisioning runs on the garage node (the
ensure-garagesystemd unit), so it re-runs on every deploy of that node and picks up the current aggregated request set — it is not an operator-side TF graph here. The "mint per-tenant valkey/garage in the operator-TF graph" model (so a shared HP node is not redeployed when an operator changes) is the target architecture from sibling branches (central-valkey,host-garage-central), not yet realized onmulti-tenant-infra. A real shared-HP rollout of the new per-tenant services should adopt that operator-TF variant: same request/applyshape, butapplyemits OpenTofu resources (e.g. a garage TF provider, an OpenBao namespace resource, a BIND zone+TSIG resource) into the operator graph keyed per tenant, rather than a systemd unit on a shared node. The two-system distinction matters:providerRequestsrouting (nix/contracts/lib/contracts/module.nix, ~444-485): groups each contract request by the provider it's routed to — the hook a TF-apply-time per-tenant minting path would use to fan requests to a per-provider operator-TF graph.valkey: no
src/resources/valkey/exists on this branch; the per-tenant valkey construction referenced in design notes lives on thecentral-valkeybranch (shared HP valkey + redis contract/external provider, per-tenant valkey minted by anoperator-tf-incus-hostsTF graph). When ported here it would follow the same request -> aggregate ->apply-emits-TF shape.Key architectural implication
The clean split: OpenBao, PostgreSQL, BIND can graduate into the shared hosting-provider group with enforced per-tenant slices (namespace / DB+role / zone+TSIG). lldap, Authelia, restic, and maddy-for-isolation must be minted one-per-tenant via the operator group / operator-TF graph, exactly like the existing garage/valkey per-tenant pattern — because (a) they have no enforced in-instance tenant boundary, and (b) per-tenant resources must track operator changes without redeploying a shared HP node. A shared hosting-provider lldap/Authelia remains valid only for HP-internal admin-UI SSO, which is a single trusted org, not a multi-tenant surface.
All of the "next step" verdicts above reuse the same construction described in the reference-pattern section: a resource
requestper consumer, generic aggregation, and anapplythat emits either a provisioning unit on a shared node (resource system, like garage today) or per-tenant OpenTofu resources in the operator graph (the central-valkey/host-garage-central target). The new enforced-isolation slices map ontoapplyoutputs directly: an OpenBao namespace, a PostgreSQL DB+role, a BIND zone + TSIG key +update-policy— each keyed per tenant.This also argues for introducing an explicit
tenantnotion insrc/lib/data-model.nixso per-tenant minting (today implicit in operator == tenant) becomes first-class and the per-tenant-vs-shared routing above is expressible.Verification
This is an assessment; "verification" = confirming the upstream claims behind each verdict before acting on one. Authoritative sources:
update-policy/TSIG/catalog zones: https://bind9.readthedocs.io/en/stable/reference.html , https://bind9.readthedocs.io/en/stable/chapter6.htmlWhen a verdict is implemented, validate via the existing apps-tf nspawn test harness (per-tenant resource minting already exercised there for garage/valkey) plus a per-service isolation subtest (e.g. tenant-A TSIG key rejected against tenant-B zone; tenant-A OpenBao token denied in tenant-B namespace).
reproduce project infrastructureto reproducible project infrastructureall up and running by now