Technical design, as one document

This page holds the technical design and each of its parts. Print the page from the browser to get one file.

Technical design

This page is the entry of the technical design of Fediversity. It gives the scope, the list of the parts, and links to the page that has the details of each subject. The other pages of this site, the site of the panel and the site of the API stay the source for their subjects. This page does not repeat them.

To get the technical design as one file, open the one-document page and print it from the browser.

Scope

Fediversity lets a hosting provider offer applications to its customers. These we call operator: a person or an organization that has a deployment with applications on its own domain. The glossary defines the terms and the actors.

The technical design covers five functions:

  1. Install applications.
  2. Configure applications.
  3. Update applications.
  4. Manage users.
  5. Switch an operator to and from a different hosting provider.

The repository has three components, each with its own flake and its own documentation site:

Component Directory What it is
core core/ Nix code: the data model, the applications and services, the resources and contracts, and the deployment programs that use Nix and OpenTofu.
api api/ A FastAPI service. It validates a configuration, puts a build in a queue, runs the deployment programs of core, and sends progress events.
panel panel/ A Django web application. It keeps the accounts and the ownership of the deployments, shows the configuration form, and answers the authorization calls of api.

api and panel use core. core does not use api or panel.

Parts of the technical design

Each page in core/docs/md/technical-design/ is one part of the technical design. The build fails when this list does not link a part.

  • Access model: the persons and the components that have access to the state of the system, and the limits of that access.
  • Architecture: the system context, the containers of an installation, and the components of each deployment.
  • Capability flows: the run-time flow of each capability, with the code paths, the stores that change and the failure paths.
  • Configuration data flow: how an operator configuration becomes a deployment.
  • Contracts (RFC 189): how the contracts connect the software that needs a service to the software that gives it.
  • Credentials register: where each credential is, what makes it, how long it lives and how it changes.
  • Data subject requests: which party answers a request about personal data, and which mechanism the code gives for it.
  • Deployment topology: the nodes of a deployment, where they run, the networks between them and the trust boundaries.
  • The five functions: how the system installs, configures and updates applications, manages users, and switches an operator to a different hosting provider.
  • Metering and accounting: what the system measures about a deployment, and where a hosting provider reads it.
  • Logs, traces and audit records: each channel that records what the software and its users do, with its personal data, store and retention.
  • Requirements of hosting providers and design review: the requirements of the hosting providers, the place in the design that answers each one, and the review of the design.
  • Quality attributes: the target of each quality attribute, what the code does, and the test that shows it.
  • State lifecycle: what happens to the state when the code changes, or when a service gets a new version.
  • State register: each store of state, its owner, its personal data, its retention and its protection.
  • Traceability of the capabilities: the requirements, the code, the tests and the docs pages of each capability.

Where to find the details

Subject Page
Terms and actors Concepts
How a configuration becomes a deployment Configuration data flow
Options of the data model, the operators and resources Data model, Operator configuration, Hosting resources
Requirements of core Specifications of core
Design and requirements of api Design of the API, specifications of api
HTTP endpoints and events of api Swagger UI, AsyncAPI
Design and requirements of panel Architecture, specifications of panel
Use of the panel Guides
Operation of a hosting provider Hosting-provider guide, Maintainer guide
Security Threat model, SBOM
Personal data and contracts Data protection, Contract templates, Legal roles
Technical documentation of the Cyber Resilience Act Technical documentation

Access model

This part gives the persons and the components that have access to the state of the system, and the limits of that access. The tables that have the label "generated" come from the code at each build of the documentation (core/docs/md/technical-design/access-model.nix). The threats and the controls are in the threat model.

Actors

Actor Definition Where the identity is
Hosting provider The organization that runs the software of this repository for its customers (Actors). It has no account of its own. Its persons are staff and administrators.
Staff A person of the hosting provider who reads the telemetry. A user in the lldap of the hosting provider, with the group fediversity-staff.
Administrator A person of the hosting provider who can administer the nodes and the shared services. A user in the lldap of the hosting provider, with the group fediversity-admins. The person logs in to the shared account admin with a short-lived SSH certificate. When no certificate is available, the holder of the offline key of ADMIN_EMERGENCY can log in to the account emergency (core/setups/nixos/users.nix).
Panel superuser An account of the panel that can see and assign the domains of all operators. A User of the panel with is_superuser. Each SSO login sets this flag and is_staff from the groups claim: a member of fediversity-admins gets them, a different user loses them (superuserGroups in panel/nix/component/oidc.nix, sync_superuser in panel/src/panel/oidc.py). A password login does not change them.
Deployer A program or a person that applies a configuration to the nodes: the CD workflows, an administrator on a workstation, and the front end (the api and Windmill), which deploys the deployments of the operators. An OpenBao token with the policy fediversity-deployer (CD and workstation) or fediversity-operator-deployer (front end), and a user of the state backend: cd, workstation-<person> or operator (core/components/hosting-provider/terraform-backend/front.nix).
Operator A customer of the hosting provider, who configures and deploys applications through the panel. A User in the database of the panel. The log-in is with a local password, or with OpenID Connect when the hosting provider sets the OIDC_* settings (panel/src/panel/settings.py).
Tenant One deployment of an operator, as an owner of names in a shared service. The code calls the name of a tenant the namespace of the deployment. The namespace is in the name of each node of the deployment and in the certificate that SPIRE gives the node.
Application user A person who uses an application of an operator. An account in the application. Each deployment has its own lldap and authelia nodes for single sign-on (core/setups/groups/operator.nix).
Node A machine of the hosting provider or of a deployment. A certificate (SVID) from SPIRE. OpenBao gives the node the policy host-<name> (core/components/hosting-provider/openbao/spire-auth.nix).

Permissions of each role

In the panel

The panel has three levels of access.

  • A visitor without an account can read the public pages.
  • An operator can read and change only the deployments for which Deployment.operator is the account of the operator (panel/src/panel/models.py). The views filter on this field.
  • A superuser has the access of an operator. A superuser can also list, claim and assign domains for all operators, and can read the audit history of each account (reads_all_history and _AUTHZ_SUPERUSER_PROCEDURES in panel/src/panel/views.py).

The admin site of Django (/admin/) shows only the User model (panel/src/panel/admin.py). The views of the panel do not use is_staff. The directory groups fediversity-staff and fediversity-admins give no permission in the panel.

In the api

The api does not keep accounts and does not decide on access. For each request, the api sends the name of the procedure, the scope and the headers to the authorization callback of the panel (authorize in api/src/fediversity_api/authz.py, AuthzCallback in panel/src/panel/views.py). The callback accepts a request only with the callback token of the api (AUTHZ_CALLBACK_TOKEN), when that token is set. The panel identifies the requester from a bearer token that the panel signed, which stays valid for API_TOKEN_TTL seconds (panel/src/panel/tokens.py).

Requester Procedures that the callback permits
No account AUTHZ_PUBLIC_PROCEDURES: the schema, the list of applications and their icons, the configuration sections and the domain terms.
Operator The procedures of AUTHZ_PROCEDURE_RULES, only for its own deployments, its own domain contact and its own audit events.
Superuser The same, and domain_assign. domain_list and domain_claim apply to the deployments of all operators.
Service token A request with the service token of the api (API_SERVICE_TOKEN) is an internal call. The callback then decides for the user that the request names. domain_terms_notice accepts only such a call (api/src/fediversity_api/routes/domains.py).

The callback denies a procedure that is not in AUTHZ_PROCEDURE_RULES. Without a callback URL, the api starts only when API_AUTHZ_ALLOW_ALL is set, and then it permits each request.

Directory groups of the hosting provider

The groups are in the lldap of the hosting provider. The section Manage users and groups of the hosting-provider guide tells how to give a group to a user.

Service fediversity-staff fediversity-admins Source
Grafana of the hosting provider The Viewer role. Grafana has one organization, thus a viewer can read the telemetry of all deployments. The Admin role. core/components/hosting-provider/telemetry/
OpenBao None. The policy fediversity-admins: read of the full KV. With ADMIN_UI_WRITE=1, also write and delete. core/components/hosting-provider/openbao/oidc.nix
OpenBao, deploy token None. A token with the policy fediversity-deployer. core/components/hosting-provider/openbao/oidc.nix
SSH to the nodes None. A certificate for the shared account admin, which is in wheel. Each node accepts the certificate when ADMIN_SSH_CA_KEYS is set. core/components/hosting-provider/openbao/ssh-certificates.nix, core/setups/nixos/users.nix
Incus None. Administrator of all containers. The authorization scriptlet of Incus refuses an OIDC user outside the group. core/components/hosting-provider/incus/default.nix
Windmill None. The UI, from which a person starts and cancels deployments. core/components/hosting-provider/windmill/default.nix
pgweb None. The UI, which connects as the postgres superuser. A member can read the databases of all deployments. core/components/hosting-provider/postgresql/default.nix
UI of Valkey None. The UI. core/components/shared/valkey/default.nix
UI of Garage None. The UI, only when ADMIN_UI_WRITE=1. core/components/hosting-provider/garage/default.nix

When ADMIN_SOURCES is set, the firewall of each node accepts the administrative ports (port 22 by default) only from those addresses (core/resources/firewall/default.nix). An operator gets no shell access to the nodes of its deployment. The persons who can read each log, trace and audit channel are in Logs, traces and audit records.

Application roles

Each deployment of an operator has its own lldap, with the same group names. The lldap of a deployment has the groups application-admins and application-moderators (core/resources/ldap/application-role-groups.nix), and the groups of each application. Each application gives a role to the members of a group at log-in. The Grafana of a deployment accepts the members of fediversity-admins in the lldap of that deployment (core/components/operator/grafana/oidc.nix).

Generated from the role-groups.nix file of each application in core/components/operator/:

Application Directory group Role in the application The first administrator is a member
mastodon mastodon-owners Owner yes
mastodon application-admins Admin yes
mastodon mastodon-admins Admin no
mastodon application-moderators Moderator no
mastodon mastodon-moderators Moderator no
peertube application-admins 0 yes
peertube peertube-admins 0 no
peertube application-moderators 1 no
peertube peertube-moderators 1 no
pixelfed application-admins admin yes
pixelfed pixelfed-admins admin no

Access of each component to each store

The tables in this section are generated from the configuration of the nodes, with the groups of core/testers/groups.nix. That evaluation knows no deployment. Thus <own deployment> shows where a name has the namespace of the deployment of the node, and <each deployment> shows a path that applies to all deployments.

Secrets in the KV of OpenBao

Each node logs in to OpenBao with its SPIRE certificate and gets the policy host-<name>. The entries of the hosting provider are below secret/data/vars/. The entries of a deployment are below secret/data/vars/operator-deployments/<namespace>/. Each entry has a public half and a secret half. The credentials register gives the lifetime and the rotation of each credential. The table gives the name of the entry when a policy has the same capabilities on the two halves, and <half>/<entry> when it has them on one half. The table gives a grant on secret/metadata/ only when the policy has no grant on the data of the same entry.

Policy Scope Capabilities Entries
fediversity-admins not in vars read *
fediversity-deployer hosting provider create, read, update secret/deployer/adminSources, secret/deployer/dnsApiToken, secret/deployer/netboxApiToken, secret/deployer/openproviderPassword, secret/deployer/openproviderUsername, secret/deployer/zonesManagerToken, secret/smarthost-upstream
fediversity-deployer hosting provider read secret/incus-store, secret/netbox, secret/octodns, public/*
fediversity-operator-deployer <each deployment> create, read, update, delete, list authelia-backups-s3, secret/authelia-operator-secrets, desec-zone, domain-claims, lldap-backups-s3, mastodon-backups-s3, secret/mastodon-operator-secrets, mastodon-s3, migration, operator-authelia-cache-backups-s3, operator-mastodon-cache-backups-s3, operator-peertube-cache-backups-s3, operator-pixelfed-cache-backups-s3, operator-valkey-cache-backups-s3, peertube-backups-s3, peertube-s3, pixelfed-backups-s3, secret/pixelfed-operator-secrets, pixelfed-s3
fediversity-operator-deployer <each deployment> deny secret/authelia-backups-restic, secret/ldap-key-operator, secret/lldap-backups-restic, secret/mastodon-backups-restic, secret/mastodon-keys, secret/mastodon-media-backup, secret/operator-authelia-cache-backups-restic, secret/operator-authelia-oidc, secret/operator-mastodon-cache-backups-restic, secret/operator-peertube-cache-backups-restic, secret/operator-pixelfed-cache-backups-restic, secret/operator-valkey-cache-backups-restic, secret/peertube-backups-restic, secret/peertube-media-backup, secret/peertube-secret, secret/pixelfed-app, secret/pixelfed-backups-restic, secret/pixelfed-media-backup, secret/pixelfed-passport
fediversity-operator-deployer <each deployment> metadata only: list, delete *
fediversity-operator-deployer <each deployment> read public/*
fediversity-operator-deployer hosting provider create, read, update, delete, list authelia-backups-s3, desec-zone, domain-claims, lldap-backups-s3, mastodon-backups-s3, mastodon-s3, operator-authelia-cache-backups-s3, operator-mastodon-cache-backups-s3, operator-peertube-cache-backups-s3, operator-pixelfed-cache-backups-s3, operator-valkey-cache-backups-s3, peertube-backups-s3, peertube-s3, pixelfed-backups-s3, pixelfed-s3
fediversity-operator-deployer hosting provider metadata only: read secret/smarthost-upstream
fediversity-operator-deployer hosting provider read secret/netbox, secret/postgresql-admin, public/*
fediversity-operator-deployer not in vars metadata only: create retained-backups/*
host-authelia hosting provider create, read, update, delete, list authelia-backups-restic, authelia-oidc, authelia-session
host-authelia hosting provider read authelia-backups-s3, secret/smtp
host-bind hosting provider create, read, update, delete, list acme-dns, bind-backups-restic, dnssec, octodns, operator-dns
host-bind hosting provider read secret/authelia-oidc, bind-backups-s3
host-garage <each deployment> metadata only: read, list, delete authelia-backups-restic, ldap-key-operator, lldap-backups-restic, mastodon-backups-restic, mastodon-keys, mastodon-media-backup, operator-authelia-cache-backups-restic, operator-authelia-oidc, operator-mastodon-cache-backups-restic, operator-peertube-cache-backups-restic, operator-pixelfed-cache-backups-restic, operator-valkey-cache-backups-restic, peertube-backups-restic, peertube-media-backup, peertube-secret, pixelfed-app, pixelfed-backups-restic, pixelfed-media-backup, pixelfed-passport
host-garage hosting provider create, read, update, delete, list authelia-backups-s3, bind-backups-s3, garage-admin, grafana-backups-s3, hosting-provider-authelia-cache-backups-s3, hosting-provider-valkey-cache-backups-s3, lldap-backups-s3, netbox-backups-s3, openbao-backups-s3, postgresql-backups-s3, terraform-backend-backups-s3, windmill-backups-s3
host-garage hosting provider read secret/acme-dns, secret/authelia-oidc, telemetry-s3
host-garage not in vars metadata only: read, list, delete retained-backups/*
host-incus hosting provider create, read, update, delete, list incus-deploy, incus-store
host-incus hosting provider read secret/authelia-oidc
host-lldap hosting provider create, read, update, delete, list ldap-bind-hosting-provider, ldap-jwt-hosting-provider, ldap-key-hosting-provider, lldap-backups-restic
host-lldap hosting provider read secret/authelia-oidc, lldap-backups-s3, secret/smtp
host-netbox hosting provider create, read, update, delete, list netbox, netbox-backups-restic
host-netbox hosting provider read secret/authelia-oidc, netbox-backups-s3, secret/smtp
host-openbao hosting provider read secret/authelia-oidc, openbao-backups-s3, secret/postgresql-admin
host-operator-authelia <own deployment> create, read, update, delete, list authelia-backups-restic, authelia-session, operator-authelia-oidc
host-operator-authelia <own deployment> read authelia-backups-s3, secret/authelia-operator-secrets, secret/node-telemetry, secret/operator-smtp
host-operator-grafana <own deployment> create, read, update, delete, list grafana-secret, metrics-query, node-telemetry
host-operator-grafana <own deployment> read secret/operator-authelia-oidc
host-operator-lldap <own deployment> create, read, update, delete, list ldap-bind-operator, ldap-jwt-operator, ldap-key-operator, ldap-roles-operator, lldap-backups-restic
host-operator-lldap <own deployment> read lldap-backups-s3, secret/node-telemetry, secret/operator-authelia-oidc, secret/operator-smtp
host-operator-mastodon <own deployment> create, read, update, delete, list mastodon-backups-restic, mastodon-keys, mastodon-media-backup, metrics
host-operator-mastodon <own deployment> read secret/ldap-roles-operator, mastodon-backups-s3, secret/mastodon-operator-secrets, mastodon-s3, secret/node-telemetry, secret/operator-authelia-oidc, secret/operator-smtp
host-operator-peertube <own deployment> create, read, update, delete, list metrics, peertube-backups-restic, peertube-media-backup, peertube-secret
host-operator-peertube <own deployment> read secret/ldap-roles-operator, secret/node-telemetry, secret/operator-authelia-oidc, secret/operator-smtp, peertube-backups-s3, peertube-s3
host-operator-pixelfed <own deployment> create, read, update, delete, list metrics, pixelfed-app, pixelfed-app-previous, pixelfed-backups-restic, pixelfed-media-backup, pixelfed-passport
host-operator-pixelfed <own deployment> read secret/ldap-roles-operator, secret/node-telemetry, secret/operator-authelia-oidc, secret/operator-smtp, pixelfed-backups-s3, secret/pixelfed-operator-secrets, pixelfed-s3
host-operator-smtp <own deployment> create, read, update, delete, list operator-smtp, operator-smtp-dkim
host-operator-smtp <own deployment> read secret/node-telemetry, secret/operator-authelia-oidc
host-operator-smtp hosting provider read secret/smarthost-upstream
host-operator-valkey <own deployment> create, read, update, delete, list operator-authelia-cache-backups-restic, operator-mastodon-cache-backups-restic, operator-peertube-cache-backups-restic, operator-pixelfed-cache-backups-restic, operator-valkey-cache-backups-restic, valkey-operator-authelia, valkey-operator-mastodon, valkey-operator-peertube, valkey-operator-pixelfed, valkey-operator-valkey
host-operator-valkey <own deployment> read secret/node-telemetry, operator-authelia-cache-backups-s3, secret/operator-authelia-oidc, operator-mastodon-cache-backups-s3, operator-peertube-cache-backups-s3, operator-pixelfed-cache-backups-s3, operator-valkey-cache-backups-s3
host-otel-collector metadata only: list ``
host-otel-collector <each deployment> read public/metrics
host-otel-collector hosting provider read secret/authelia-oidc, secret/telemetry-ingest
host-postgresql hosting provider create, read, update, delete, list postgresql-admin, postgresql-backups-restic
host-postgresql hosting provider read secret/authelia-oidc, postgresql-backups-s3
host-smtp hosting provider create, read, update, delete, list smtp, smtp-dkim
host-smtp hosting provider read secret/authelia-oidc, secret/smarthost-upstream
host-telemetry metadata only: list ``
host-telemetry <each deployment> read public/metrics-query/htpasswd, public/node-telemetry/htpasswd
host-telemetry hosting provider create, read, update, delete, list audit-ingest-api, grafana-backups-restic, telemetry-grafana, telemetry-grafana-previous, telemetry-ingest, telemetry-s3
host-telemetry hosting provider read secret/authelia-oidc, grafana-backups-s3, secret/smtp
host-terraform-backend hosting provider create, read, update, delete, list terraform-backend-operator
host-terraform-backend hosting provider read secret/authelia-oidc, terraform-backend-backups-s3
host-valkey hosting provider create, read, update, delete, list hosting-provider-authelia-cache-backups-restic, hosting-provider-valkey-cache-backups-restic, valkey-hosting-provider-authelia, valkey-hosting-provider-valkey
host-valkey hosting provider read secret/authelia-oidc, hosting-provider-authelia-cache-backups-s3, hosting-provider-valkey-cache-backups-s3
host-windmill hosting provider create, read, update, delete, list windmill, windmill-backups-restic, windmill-superadmin
host-windmill hosting provider read secret/authelia-oidc, public/domain-claims, secret/incus-deploy, secret/incus-store, secret/operator-dns, secret/deployer/adminSources, secret/deployer/dnsApiToken, secret/deployer/netboxApiToken, secret/deployer/openbaoToken, secret/deployer/openproviderPassword, secret/deployer/openproviderUsername, secret/garage-admin/adminToken, secret/terraform-backend-operator/password, secret/smtp, windmill-backups-s3

The policy fediversity-admins in this table is that of the evaluation, where ADMIN_UI_WRITE is not set.

Other paths in OpenBao

These paths give certificates (pki), database passwords (database, postgresql), directory passwords (ldap, ldap-operator) and child tokens.

Policy Path Capabilities
fediversity-admins identity/* read, list
fediversity-admins sys/auth read
fediversity-admins sys/mounts read
fediversity-admins sys/policies/acl read, list
fediversity-admins sys/policies/acl/* read
fediversity-deployer auth/token/create update
fediversity-operator-deployer postgresql/static-roles/operator-authelia-* create, read, update, delete
fediversity-operator-deployer postgresql/static-roles/operator-lldap-* create, read, update, delete
fediversity-operator-deployer postgresql/static-roles/operator-mastodon-* create, read, update, delete
fediversity-operator-deployer postgresql/static-roles/operator-peertube-* create, read, update, delete
fediversity-operator-deployer postgresql/static-roles/operator-pixelfed-* create, read, update, delete
fediversity-operator-deployer ldap-operator/<each deployment>/rotate-role/authelia-bind update
fediversity-operator-deployer auth/token/create update
host-authelia pki/issue/internal update
host-authelia database/static-creds/hosting-provider-authelia read
host-authelia ldap/static-cred/authelia-bind read
host-authelia postgresql/static-creds/hosting-provider-authelia read
host-bind pki/issue/internal update
host-garage pki/issue/internal update
host-incus pki/issue/internal update
host-lldap pki/issue/internal update
host-lldap postgresql/static-creds/hosting-provider-lldap read
host-netbox pki/issue/internal update
host-netbox postgresql/static-creds/hosting-provider-netbox read
host-openbao pki/issue/internal update
host-operator-authelia pki/issue/internal update
host-operator-authelia database/static-creds/operator-authelia-<own deployment> read
host-operator-authelia ldap-operator/<own deployment>/static-cred/authelia-bind read
host-operator-authelia postgresql/static-creds/operator-authelia-<own deployment> read
host-operator-grafana pki/issue/internal update
host-operator-lldap pki/issue/internal update
host-operator-lldap postgresql/static-creds/operator-lldap-<own deployment> read
host-operator-mastodon pki/issue/internal update
host-operator-mastodon database/static-creds/operator-mastodon-<own deployment> read
host-operator-mastodon postgresql/static-creds/operator-mastodon-<own deployment> read
host-operator-peertube pki/issue/internal update
host-operator-peertube database/static-creds/operator-peertube-<own deployment> read
host-operator-peertube postgresql/static-creds/operator-peertube-<own deployment> read
host-operator-pixelfed pki/issue/internal update
host-operator-pixelfed database/static-creds/operator-pixelfed-<own deployment> read
host-operator-pixelfed postgresql/static-creds/operator-pixelfed-<own deployment> read
host-operator-smtp pki/issue/internal update
host-operator-valkey pki/issue/internal update
host-operator-valkey database/static-creds/operator-valkey-<own deployment> read
host-operator-valkey database/static-creds/operator-authelia-<own deployment> read
host-operator-valkey database/static-creds/operator-mastodon-<own deployment> read
host-operator-valkey database/static-creds/operator-peertube-<own deployment> read
host-operator-valkey database/static-creds/operator-pixelfed-<own deployment> read
host-otel-collector pki/issue/internal update
host-postgresql pki/issue/internal update
host-smtp pki/issue/internal update
host-telemetry pki/issue/internal update
host-terraform-backend pki/issue/internal update
host-valkey pki/issue/internal update
host-valkey database/static-creds/hosting-provider-valkey read
host-valkey database/static-creds/hosting-provider-authelia read
host-windmill pki/issue/internal update
host-windmill postgresql/static-creds/hosting-provider-windmill read

PostgreSQL

The database engine of OpenBao owns the password of each role and changes it at the interval in the table. A node reads the password of its role from OpenBao (core/setups/wiring/postgresql-roster.nix). The database and the role of a deployment have the namespace of the deployment as a suffix, and the OpenTofu graph of the deployment creates and drops them (core/effects/tf/incus/operator/postgresql.tf).

Nodes that read the password Role Database Scope Credential
authelia hosting-provider-authelia hosting-provider-authelia hosting provider rotating, 24h
lldap hosting-provider-lldap hosting-provider-lldap hosting provider rotating, 24h
netbox hosting-provider-netbox hosting-provider-netbox hosting provider rotating, 24h
windmill hosting-provider-windmill hosting-provider-windmill hosting provider rotating, 24h
operator-authelia operator-authelia-<own deployment> operator-authelia-<own deployment> each operator deployment rotating, 24h
operator-lldap operator-lldap-<own deployment> operator-lldap-<own deployment> each operator deployment rotating, 24h
operator-mastodon operator-mastodon-<own deployment> operator-mastodon-<own deployment> each operator deployment rotating, 24h
operator-peertube operator-peertube-<own deployment> operator-peertube-<own deployment> each operator deployment rotating, 24h
operator-pixelfed operator-pixelfed-<own deployment> operator-pixelfed-<own deployment> each operator deployment rotating, 24h

The postgresql node also has these group roles:

Group role Members Privileges on the database Privileges on the objects Bypasses row-level security
windmill_admin hosting-provider-windmill each sequence of hosting-provider-windmill: ALL; each table of hosting-provider-windmill: ALL yes
windmill_user windmill_admin hosting-provider-windmill: CONNECT, CREATE, TEMPORARY each sequence of hosting-provider-windmill: ALL; each table of hosting-provider-windmill: ALL no

Two actors have access to all databases of the node: the front-end deployer, which reads the password of the postgres superuser (the entry secret/postgresql-admin in the KV table), and a member of fediversity-admins through pgweb.

Buckets in Garage

Each row is one ensureAccess entry of a key that a node requests. owner lets the key change the configuration of the bucket.

Group Node Key Bucket Access
hosting-provider authelia hosting-provider-authelia-backups hosting-provider-authelia-backups read, write, owner
hosting-provider authelia hosting-provider-authelia-cache-backups hosting-provider-authelia-cache-backups read, write, owner
hosting-provider bind hosting-provider-bind-backups hosting-provider-bind-backups read, write, owner
hosting-provider lldap hosting-provider-lldap-backups hosting-provider-lldap-backups read, write, owner
hosting-provider netbox hosting-provider-netbox-backups hosting-provider-netbox-backups read, write, owner
hosting-provider openbao hosting-provider-openbao-backups hosting-provider-openbao-backups read, write, owner
hosting-provider postgresql hosting-provider-postgresql-backups hosting-provider-postgresql-backups read, write, owner
hosting-provider telemetry hosting-provider-grafana-backups hosting-provider-grafana-backups read, write, owner
hosting-provider telemetry hosting-provider-telemetry hosting-provider-loki read, write, owner
hosting-provider telemetry hosting-provider-telemetry hosting-provider-mimir read, write, owner
hosting-provider telemetry hosting-provider-telemetry hosting-provider-tempo read, write, owner
hosting-provider terraform-backend hosting-provider-terraform-backend-backups hosting-provider-terraform-backend-backups read, write, owner
hosting-provider valkey hosting-provider-valkey-cache-backups hosting-provider-valkey-cache-backups read, write, owner
hosting-provider windmill hosting-provider-windmill-backups hosting-provider-windmill-backups read, write, owner
operator authelia <own deployment>-authelia-backups <own deployment>-authelia-backups read, write, owner
operator authelia <own deployment>-authelia-cache-backups <own deployment>-authelia-cache-backups read, write, owner
operator lldap <own deployment>-lldap-backups <own deployment>-lldap-backups read, write, owner
operator mastodon <own deployment>-mastodon <own deployment>-mastodon read, write, owner
operator mastodon <own deployment>-mastodon-backups <own deployment>-mastodon read
operator mastodon <own deployment>-mastodon-backups <own deployment>-mastodon-backups read, write, owner
operator mastodon <own deployment>-mastodon-cache-backups <own deployment>-mastodon-cache-backups read, write, owner
operator peertube <own deployment>-peertube <own deployment>-peertube-playlists read, write, owner
operator peertube <own deployment>-peertube <own deployment>-peertube-videos read, write, owner
operator peertube <own deployment>-peertube-backups <own deployment>-peertube-backups read, write, owner
operator peertube <own deployment>-peertube-backups <own deployment>-peertube-playlists read
operator peertube <own deployment>-peertube-backups <own deployment>-peertube-videos read
operator peertube <own deployment>-peertube-cache-backups <own deployment>-peertube-cache-backups read, write, owner
operator pixelfed <own deployment>-pixelfed <own deployment>-pixelfed read, write, owner
operator pixelfed <own deployment>-pixelfed-backups <own deployment>-pixelfed read
operator pixelfed <own deployment>-pixelfed-backups <own deployment>-pixelfed-backups read, write, owner
operator pixelfed <own deployment>-pixelfed-cache-backups <own deployment>-pixelfed-cache-backups read, write, owner
operator valkey <own deployment>-valkey-cache-backups <own deployment>-valkey-cache-backups read, write, owner

How the system keeps the tenants apart

  • Nodes. Each deployment has its own nodes for each application and for valkey, authelia, smtp, lldap and grafana (core/setups/groups/operator.nix). The users, the sessions and the cache of a deployment are thus not in a store that a different deployment uses.
  • Network. All nodes are on one Incus bridge. Each deployment gets a /28 of its own, and an Incus network ACL on each NIC drops the traffic to and from the other deployments (core/effects/tf/incus/operator/bridge-isolation.tf). The Incus host declares the ACL of each /28 (core/components/hosting-provider/incus-bridge-acls.nix), thus the certificate of a deploy runner is restricted to the Incus project of the deployments and cannot change an ACL. Each NIC has MAC and IP address filtering (core/effects/tf/incus/instance.tf). A second ACL and the firewall of each node of the hosting provider permit only the ports that a deployment uses (core/components/hosting-provider/operator-reach.nix). The second ACL holds only the IPv4 addresses of the nodes of the hosting provider. The bridge has no IPv6 subnet (ipv6.address = none in core/components/hosting-provider/incus.nix), thus Incus drops all IPv6 on eth0. A node of a deployment sends IPv6 only on its public eth1, which has no NAT, thus the firewall of the destination node sees the real source address and applies the port limit.
  • Secrets. The policy of a node of a deployment names the KV paths with a template that OpenBao replaces with the namespace from the certificate of the node. Thus a node reads only the entries of its own deployment (core/components/hosting-provider/openbao/spire-auth.nix).
  • Databases. All deployments use the PostgreSQL node of the hosting provider. Each deployment has its own database and role for each application, and a node can read only the password of the role that has its own namespace.
  • Buckets. All deployments use the Garage cluster of the hosting provider. Each bucket and key has the namespace of the deployment as a prefix (tenant in core/resources/garage/default.nix), and a key has access only to the buckets in the table above.
  • Metrics. The query path for the metrics of an application adds the deployment_id label to each query (core/components/hosting-provider/metrics-query.nix).
  • Panel and api. An operator gets only the deployments that it owns (see In the panel).

These components are common to all tenants, and the separation there is weaker:

  • The front-end deployer is one identity for all deployments. Its OpenBao policy can write the entries of each deployment that the KV table gives, and it can read the password of the postgres superuser. The state backend has one user operator for the states of all deployments (core/components/hosting-provider/terraform-backend/front.nix).
  • The Grafana of the hosting provider has one organization, thus fediversity-staff and fediversity-admins can read the telemetry of all deployments.
  • A member of fediversity-admins can read the secrets, the databases and the containers of all deployments.

Architecture

This part shows the system context of Fediversity and the containers of one installation of a hosting provider. It also shows the components of each deployment, with the services that each component requests and provides.

System context

flowchart TB
  operator["<b>Operator</b><br/>[Person]<br/>Has a deployment with applications"]
  user["<b>Application user</b><br/>[Person]<br/>Uses an application of an operator"]
  provider["<b>Hosting provider</b><br/>[Person]<br/>Runs the infrastructure and manages the accounts"]
  maintainer["<b>Maintainer</b><br/>[Person]<br/>Changes and releases the code"]

  fediversity["<b>Fediversity</b><br/>[Software system]<br/>Installs, configures and updates applications, manages users, moves deployments between hosting providers"]

  registrar["<b>Domain registrar</b><br/>[External system]<br/>OpenProvider, INWX or Porkbun"]
  dns["<b>DNS service</b><br/>[External system]<br/>Authoritative DNS that is not in the deployment, for example deSEC or PowerDNS"]
  acme["<b>ACME certificate authority</b><br/>[External system]<br/>Issues TLS certificates"]
  relay["<b>Mail relay</b><br/>[External system]<br/>SMTP submission service of a third party"]
  upstream["<b>Upstream sources</b><br/>[External system]<br/>nixpkgs and the other pinned sources"]
  other["<b>Other hosting provider</b><br/>[Software system]<br/>A different installation of Fediversity"]

  operator -->|"configures and deploys, HTTPS"| fediversity
  user -->|"uses the applications, HTTPS"| fediversity
  provider -->|"configures, deploys and monitors"| fediversity
  maintainer -->|"releases code and pins"| fediversity
  fediversity -->|"registers and transfers domains"| registrar
  fediversity -->|"publishes DNS records"| dns
  fediversity -->|"requests certificates"| acme
  fediversity -->|"sends mail"| relay
  fediversity -->|"gets packages and modules"| upstream
  fediversity -->|"exports and imports a deployment"| other

  classDef external stroke-dasharray: 5 5
  class registrar,dns,acme,relay,upstream,other external

Each external system is a selection of the hosting provider, not a fixed part:

  • Registrars: core/effects/tf/registrars.nix has one entry for each registrar, and the steps that each registrar can do.
  • DNS: the dns resource has a provider for a BIND node in the deployment (core/resources/dns/self-hosted.nix) and a provider for a service that the hosting provider already has (core/resources/dns/external.nix). The core/effects/octodns effect publishes the records. Choose where the DNS zones are gives the selection.
  • Certificates: the ssl resource has an ACME provider, an OpenBao PKI provider and a self-signed provider (core/resources/ssl/).
  • Mail: the smtp resource has a provider that runs a mail server in the deployment and a provider for an external relay (core/resources/smtp/).
  • Upstream sources: npins/sources.json pins nixpkgs and each other source. The SBOM page tells how to find the software of a node.

The threat model gives the trust boundaries between these parties, and data protection gives the parties that get personal data.

Containers

The diagram shows the parts of one installation of a hosting provider. The architecture page of the panel has the container and component views of panel, and the site of the API has those of api.

flowchart TB
  browser["<b>Browser of the operator</b><br/>[Person and browser]"]

  subgraph frontend["Front end"]
    panel["<b>panel</b><br/>[Container: Django]<br/>Accounts, ownership of deployments, configuration form"]
    panelDb[("<b>Panel database</b><br/>[Container: PostgreSQL or SQLite]")]
  end

  subgraph apiSide["API"]
    api["<b>api</b><br/>[Container: FastAPI]<br/>Validates, queues builds, runs deployments"]
    apiDb[("<b>API state store</b><br/>[Container: PostgreSQL or SQLite]")]
    centrifugo["<b>Centrifugo</b><br/>[Container]<br/>Sends progress events"]
    jobs["<b>Job runner</b><br/>[Container: subprocess or Windmill]<br/>Runs a build or a deployment"]
  end

  subgraph coreSide["core"]
    programs["<b>Deployment programs</b><br/>[Container: Nix and OpenTofu]<br/>Change a configuration into machines, records and secrets"]
    schema["<b>Schema and catalog</b><br/>[Container: Nix]<br/>JSON Schema of the options, list of applications"]
  end

  subgraph providerSide["Deployment of the hosting provider"]
    services["<b>Shared services</b><br/>[Containers]<br/>See the diagrams below"]
    incus["<b>Incus hypervisor</b><br/>[Container]<br/>Runs the machines of the deployments"]
  end

  subgraph operatorSide["Deployment of an operator"]
    apps["<b>Applications and their services</b><br/>[Containers]<br/>See the diagrams below"]
  end

  browser -->|"HTTPS"| panel
  browser -->|"HTTPS, bearer token"| api
  centrifugo -->|"WebSocket"| browser
  panel --> panelDb
  api -->|"authorization callback, HTTPS"| panel
  api --> apiDb
  api -->|"publishes events"| centrifugo
  api -->|"reads"| schema
  api -->|"starts a job"| jobs
  jobs -->|"runs"| programs
  programs -->|"creates and changes machines"| incus
  programs -->|"reads and writes state, secrets and addresses"| services
  incus -->|"runs"| apps
  apps -->|"use"| services

Notes for the diagram:

  • api runs a deployment program as a nix subprocess, directly or in a Windmill job (api/src/fediversity_api/_runner.py, api/src/fediversity_api/deployment.py).
  • api and panel are also components of the data model (api/nix/component/, panel/nix/component/), thus the deployment programs of core can deploy them the same as each other service.
  • Configuration data flow shows how a configuration becomes a deployment in core.
  • Flake attributes lists the deployment programs.

Components of a deployment of an operator

The build makes the two diagrams and the two tables below from the code. They show the components of the operator group and of the hosting-provider group of core/setups/groups, each with its default settings. A node shows the resources that the component provides (wiring.provides in core/components/wiringProperties.nix). An arrow goes from a component to the component that answers its request, and its label gives the resources. A request goes to a component of the same group. If the group has no such component, the request goes to the component of the hosting-provider group.

Two requests have a name that is different from the name of the backend that answers them: a generateFiles request goes to varsBackend, and a garage request goes to objectStore. A request without an arrow has no service component that provides it. Examples are nixos-module and firewall, which change the machine itself, and ssl, which has three providers. Hosting resources describes each resource.

flowchart LR
  subgraph applications["Applications that the operator selects"]
    applications_mastodon["<b>mastodon<br/>peertube<br/>pixelfed</b>"]
  end
  subgraph own["Service components of the operator group"]
    own_authelia["<b>authelia</b><br/>provides: oidc"]
    own_grafana["<b>grafana</b>"]
    own_lldap["<b>lldap</b><br/>provides: ldap"]
    own_smtp["<b>smtp</b><br/>provides: smtp"]
    own_valkey["<b>valkey</b><br/>provides: redis"]
  end
  subgraph central["Service components of the hosting-provider group"]
    central_garage["<b>garage</b><br/>provides: objectStore"]
    central_openbao["<b>openbao</b><br/>provides: varsBackend, pki"]
    central_postgresql["<b>postgresql</b><br/>provides: postgresql"]
    central_spire["<b>spire</b><br/>provides: spiffe"]
  end
  applications_mastodon -->|"garage"| central_garage
  applications_mastodon -->|"generateFiles"| central_openbao
  applications_mastodon -->|"postgresql"| central_postgresql
  applications_mastodon -->|"spiffe"| central_spire
  applications_mastodon -->|"oidc"| own_authelia
  applications_mastodon -->|"ldap"| own_lldap
  applications_mastodon -->|"smtp"| own_smtp
  applications_mastodon -->|"redis"| own_valkey
  own_authelia -->|"garage"| central_garage
  own_authelia -->|"generateFiles"| central_openbao
  own_authelia -->|"postgresql"| central_postgresql
  own_authelia -->|"spiffe"| central_spire
  own_authelia -->|"redis"| own_valkey
  own_grafana -->|"generateFiles"| central_openbao
  own_grafana -->|"spiffe"| central_spire
  own_grafana -->|"oidc"| own_authelia
  own_lldap -->|"garage"| central_garage
  own_lldap -->|"generateFiles"| central_openbao
  own_lldap -->|"postgresql"| central_postgresql
  own_lldap -->|"spiffe"| central_spire
  own_lldap -->|"oidc"| own_authelia
  own_smtp -->|"spiffe"| central_spire
  own_valkey -->|"spiffe"| central_spire
  own_valkey -->|"oidc"| own_authelia
Component Kind Provides Requests
authelia service oidc garage, postgresql, generateFiles, redis, nixos-module, spiffe, firewall
grafana service -- nixos-module, oidc, spiffe, ssl, generateFiles, firewall
lldap service ldap garage, postgresql, oidc, nixos-module, spiffe, generateFiles
mastodon application -- nixos-module, garage, redis, postgresql, oidc, ldap, smtp, generateFiles, pod-roles, spiffe, ssl, firewall
peertube application -- nixos-module, firewall, generateFiles, garage, redis, postgresql, oidc, ldap, smtp, spiffe, ssl
pixelfed application -- nixos-module, generateFiles, garage, redis, postgresql, oidc, ldap, smtp, spiffe, ssl, firewall
smtp service smtp spiffe
valkey service redis nixos-module, oidc, spiffe, redis

Components of the deployment of the hosting provider

flowchart LR
  subgraph own["Service components of the hosting-provider group"]
    own_authelia["<b>authelia</b><br/>provides: oidc"]
    own_bind["<b>bind</b>"]
    own_garage["<b>garage</b><br/>provides: objectStore"]
    own_incus["<b>incus</b><br/>provides: instances"]
    own_lldap["<b>lldap</b><br/>provides: ldap"]
    own_netbox["<b>netbox</b><br/>provides: netbox"]
    own_openbao["<b>openbao</b><br/>provides: varsBackend, pki"]
    own_otel_collector["<b>otel-collector</b><br/>provides: otel"]
    own_overview["<b>overview</b><br/>provides: uiIndex"]
    own_postgresql["<b>postgresql</b><br/>provides: postgresql"]
    own_smtp["<b>smtp</b><br/>provides: smtp"]
    own_spire["<b>spire</b><br/>provides: spiffe"]
    own_telemetry["<b>telemetry</b><br/>provides: otelBackend"]
    own_terraform_backend["<b>terraform-backend</b><br/>provides: tfState"]
    own_valkey["<b>valkey</b><br/>provides: redis"]
    own_windmill["<b>windmill</b>"]
  end
  own_authelia -->|"garage"| own_garage
  own_authelia -->|"generateFiles"| own_openbao
  own_authelia -->|"postgresql"| own_postgresql
  own_authelia -->|"spiffe"| own_spire
  own_authelia -->|"redis"| own_valkey
  own_bind -->|"garage"| own_garage
  own_bind -->|"generateFiles"| own_openbao
  own_bind -->|"spiffe"| own_spire
  own_garage -->|"oidc"| own_authelia
  own_garage -->|"spiffe"| own_spire
  own_incus -->|"oidc"| own_authelia
  own_incus -->|"generateFiles"| own_openbao
  own_incus -->|"spiffe"| own_spire
  own_lldap -->|"oidc"| own_authelia
  own_lldap -->|"garage"| own_garage
  own_lldap -->|"generateFiles"| own_openbao
  own_lldap -->|"postgresql"| own_postgresql
  own_lldap -->|"spiffe"| own_spire
  own_netbox -->|"oidc"| own_authelia
  own_netbox -->|"garage"| own_garage
  own_netbox -->|"generateFiles"| own_openbao
  own_netbox -->|"otel"| own_otel_collector
  own_netbox -->|"postgresql"| own_postgresql
  own_netbox -->|"smtp"| own_smtp
  own_netbox -->|"spiffe"| own_spire
  own_openbao -->|"oidc"| own_authelia
  own_openbao -->|"garage"| own_garage
  own_openbao -->|"spiffe"| own_spire
  own_otel_collector -->|"generateFiles"| own_openbao
  own_otel_collector -->|"spiffe"| own_spire
  own_overview -->|"spiffe"| own_spire
  own_postgresql -->|"oidc"| own_authelia
  own_postgresql -->|"garage"| own_garage
  own_postgresql -->|"generateFiles"| own_openbao
  own_postgresql -->|"spiffe"| own_spire
  own_smtp -->|"spiffe"| own_spire
  own_telemetry -->|"oidc"| own_authelia
  own_telemetry -->|"garage"| own_garage
  own_telemetry -->|"generateFiles"| own_openbao
  own_telemetry -->|"smtp"| own_smtp
  own_telemetry -->|"spiffe"| own_spire
  own_terraform_backend -->|"garage"| own_garage
  own_terraform_backend -->|"generateFiles"| own_openbao
  own_terraform_backend -->|"spiffe"| own_spire
  own_valkey -->|"oidc"| own_authelia
  own_valkey -->|"spiffe"| own_spire
  own_windmill -->|"oidc"| own_authelia
  own_windmill -->|"garage"| own_garage
  own_windmill -->|"netbox"| own_netbox
  own_windmill -->|"generateFiles"| own_openbao
  own_windmill -->|"otel"| own_otel_collector
  own_windmill -->|"postgresql"| own_postgresql
  own_windmill -->|"smtp"| own_smtp
  own_windmill -->|"spiffe"| own_spire
Component Kind Provides Requests
authelia service oidc garage, postgresql, generateFiles, redis, nixos-module, spiffe, firewall
bind service -- garage, nixos-module, spiffe, generateFiles
garage service objectStore nixos-module, oidc, spiffe, firewall
incus service instances nixos-module, generateFiles, oidc, spiffe, ssl
lldap service ldap garage, postgresql, oidc, nixos-module, spiffe, generateFiles
netbox service netbox nixos-module, garage, postgresql, oidc, smtp, otel, generateFiles, spiffe, ssl
openbao service varsBackend, pki nixos-module, garage, oidc, spiffe, ssl
otel-collector service otel nixos-module, spiffe, generateFiles
overview service uiIndex nixos-module, spiffe, ssl, firewall
postgresql service postgresql nixos-module, oidc, garage, spiffe, generateFiles
smtp service smtp spiffe
spire service spiffe nixos-module
telemetry service otelBackend generateFiles, nixos-module, garage, spiffe, oidc, smtp, ssl
terraform-backend service tfState nixos-module, generateFiles, spiffe, garage, ssl
valkey service redis nixos-module, oidc, spiffe, redis
windmill service -- generateFiles, nixos-module, dns, netbox, otel, spiffe, ssl, garage, postgresql, smtp, oidc

api and panel are not in this table, because their component definitions are outside core.

Capability flows

This part gives the run-time flow of each capability: install, configure and update an application, manage users, and switch to and from a different hosting provider. Each flow has a sequence diagram, the code paths, the stores that change, the failure paths, and the work that is planned.

The flows have these participants:

  • The operator is the customer of the hosting provider. The operator uses a browser.
  • panel is the Django front-end (panel/). It keeps the accounts, the stored configurations and the permissions.
  • api is the FastAPI service (api/). It validates a configuration and starts a deploy.
  • core is the Nix and OpenTofu code (core/). It makes the schema, and it deploys.

The design of the API gives the contract of each call. The architecture of the panel gives the design of the panel.

Calls that each flow uses

Each call from the browser to api uses the same permission check:

  1. The browser gets a bearer token from panel at api/token/ (views.ApiToken, panel/src/panel/tokens.py). panel signs the token with its SECRET_KEY. The token is valid for API_TOKEN_TTL.
  2. The browser sends the call to api with the token.
  3. api does not examine the token. It sends the procedure name, the scope and the headers to the callback of panel at api/authz/ (api/src/fediversity_api/authz.py, views.AuthzCallback).
  4. panel answers allow, and for an allowed call the subject and the deployments that the caller can use. panel denies a procedure that has no rule in AUTHZ_PROCEDURE_RULES.
  5. If panel denies the call, api answers authz_denied (HTTP 403).

The diagrams show this check as one step, "check the permission".

Install an application

The operator fills in the form of a deployment, enables an application, and deploys. The same call does the first install and each later change.

sequenceDiagram
    actor O as Operator
    participant P as panel
    participant A as api
    participant C as core
    participant I as Provider infrastructure
    participant N as Node
    O->>P: save the form
    P->>P: store the configuration
    O->>A: POST /deployments/submit/{id}
    A->>P: check the permission
    A->>C: build the schema of the module source
    A->>A: migrate renamed options, validate
    A->>C: evaluate the names of the deployment
    A->>A: claim the domain, store the build as queued
    A-->>O: build_id, status queued, position
    A->>C: run tf-run.sh
    C->>I: NetBox address, Incus instance and volume
    C->>I: bucket, database, secrets in OpenBao
    C->>I: hold the zone, register the domain, publish the records
    C->>N: switch to the NixOS configuration
    N->>N: order certificates with ACME, start the services
    C->>N: read the state of the units
    C-->>A: exit code and outputs
    A-->>O: build events through Centrifugo

Code paths:

  • panel: views.DeploymentForm shows only the deployments of the operator. static/deployment_submit.js saves the form in panel, then calls api.
  • api: the route deployment_submit (routes/deployments.py) calls procedures.deployment_submit. In sequence, this function:
    1. refuses a module source that is not on the trusted-source allowlist (source_allowlist.py),
    2. builds the schema and the rename table of the module source (schema_build.py),
    3. applies the renames and validates the configuration with jsonschema,
    4. takes the secrets of the operator out of the configuration (operator_secrets.py),
    5. refuses a full queue,
    6. evaluates the names of the deployment and claims the apex of the domain (apex_claims.py),
    7. makes the DNS token of the zone (zone_tokens.py), writes the secrets of the operator to OpenBao, and stores the deployment (state.upsert_deployment),
    8. starts the build (deployment.start_deployment).
  • The build runs as a subprocess of api, or as a Windmill job when windmill_base_url is set (deployment.py). The runner starts nix develop .#<attribute> --impure --command tf-run.sh (_runner.py).
  • core: tf-run.sh (core/effects/common/lib.nix, core/effects/tf/common/bootstrap-apply.sh) applies the OpenTofu graph of core/effects/tf/incus/operator/, then gives the outputs as JSON. The state of the graph is in the HTTP state backend.
  • DNS: dns-lane-zone.tf and registrar.tf hold the zone, register the domain and wait until the parent zone delegates. dns-lane-records.tf publishes the names of each node and waits until the names resolve. All of this occurs before the switch of the node, because the certificate order of the node needs the records.
  • TLS: core/resources/ssl/acme.nix sets security.acme. A node orders the certificate of an ordinary name with the HTTP challenge, and the certificate of a wildcard name with the DNS-01 challenge.
  • After the switch, null_resource.deploy_verdict reads the units of each node. A node with a unit that continues to restart fails the deploy.

Stores that change:

Store Change
Database of panel The stored configuration of the deployment.
Database of api The deployment, its build and the audit event.
OpenBao KV The secrets of the operator, the S3 keys, the claim of the domain.
OpenTofu state Each resource of the graph.
NetBox The address and the virtual machine record.
Incus The instance and its persistent volume.
Garage, PostgreSQL The bucket and its key, the database and its role.
DNS zone, registrar The records of the nodes, the registration of the domain.

Failure paths:

  • A configuration that fails the schema gives invalid_configuration (HTTP 400). Nothing is stored.
  • A full queue gives queue_full (HTTP 409). A domain that a different deployment holds gives conflict (HTTP 409).
  • A schema build that passes its time limit gives build_timeout (HTTP 504).
  • A deploy that fails sets the build to failed. api publishes the end of the log. The resources that the apply made stay in the state, thus the next submit continues from them.
  • When api starts again, reconcile_orphaned_builds (deployment.py) marks the builds that lost their process.

Planned:

  • Builds of one deployment that run in parallel (issue 242 of fediversity/fediversity).
  • The configuration of the deploy method in the panel (issues 723 and 1071).

Configure an application

The form of an application comes from the options of its NixOS module. No form is written by hand.

sequenceDiagram
    actor O as Operator
    participant F as Form in the browser
    participant P as panel
    participant A as api
    participant C as core
    O->>P: open the form of the deployment
    P-->>F: stored, deployed and default module source
    F->>A: GET /schema and /schema/migrations
    A->>C: build provider-configuration-schema and configuration-renames
    C-->>A: JSON Schema and rename table
    A-->>F: JSON Schema and rename table
    F->>F: apply the renames, show the form
    O->>F: change a value
    F->>F: validate with AJV
    O->>F: deploy
    F->>P: save the configuration
    F->>A: POST /deployments/submit/{id}
    A->>A: apply the renames, validate with jsonschema
    A->>C: run tf-run.sh

Code paths:

  • Schema: core/lib/json-schema.nix makes a JSON Schema from the options of frontendConfiguration (core/lib/data-model.nix). Add an application and the data model give the options.
  • api: schema_build.py builds the outputs provider-configuration-schema and configuration-renames of the module source. A build has a time limit, and api limits the number of builds that run together. api keeps the result for each source in memory. The routes are in routes/schema.py.
  • Form: panel/forms-island-adapter/panel-adapter.ts gets the schema and the rename table, validates each change with AJV, and shows the difference between the schema of the stored source and the schema of the source that is deployed.
  • Validation occurs two times: in the browser with AJV, and in api with jsonschema (procedures._checked_submission). The check in api is the authority.
  • Apply: the apply is the submit of the install flow. A changed option changes the NixOS configuration of a node, and the deploy switches the node to it.

Stores that change: the stored configuration in panel, then the stores of the install flow.

Failure paths:

  • A module source that is not on the allowlist is refused before a build.
  • A configuration that fails the schema gives invalid_configuration.
  • POST /deployments/preview/{id} does the checks of the submit and shows the nodes that a submit deploys. It deploys nothing.

Planned:

  • Resource versions, which let api find a difference between the environments of the provider and the pinned data model before a deploy (issue 634).
  • To follow the new revisions of a fork of the module source (issue 3053).

Update an application

An update is a submit with a newer module source. The module source selects the revision of the application modules.

sequenceDiagram
    actor O as Operator
    participant P as panel
    participant A as api
    participant C as core
    participant N as Node
    participant S as Object store
    alt policy manual
        P-->>O: notice that a newer revision exists
        O->>A: POST /deployments/submit/{id} with the update applied
    else policy automatic or window
        A->>A: the scheduler submits the stored configuration again
    end
    A->>A: resolve the null source to the revision of the provider
    A->>A: apply the renames, validate
    A->>C: run tf-run.sh
    C->>N: switch to the new NixOS configuration
    N->>N: guard reads the recorded version
    alt downgrade or skipped release series
        N-->>C: the guard fails, the service does not start
    else upgrade
        N->>N: stop the service, dump the database
        N->>S: restic snapshot of the dump
        N->>N: start the service, run the migrations after the deployment
        N->>N: record the new version
    end
    C->>N: read the state of the units

Code paths:

  • Policy: api/src/fediversity_api/update_policy.py has the policies manual, automatic and window. The operator sets the policy with PUT /deployments/update_policy/{id} (views.DeploymentUpdatePolicy, static/update_policy.js). static/update_notice.js shows that a newer revision exists. The form has a check box beside the deploy button: when it is off, the submit has apply_update set to false (static/deployment_submit.js) and api keeps the deployed revision.
  • Scheduler: automatic_updates.run_scheduler submits the stored configuration with a null module source. It tries each revision of the provider one time for each deployment (state.claim_update_attempt), and writes the audit action automatic_update. It does not update a deployment that is suspended or not active. A deployment whose deployed module source is not the source of the provider is due too (run_due_updates), thus the scheduler moves a deployment from a fork to the source of the provider.
  • Version boundary: version_boundary.py selects, for the layers core/resources and core/effects, the newer of the pinned revision and the revision of the provider.
  • Guards: each application has a systemd.stateMigrations entry. core/components/operator/mastodon/migrations.nix records the version, refuses a downgrade or a skipped minor release series, dumps the database before an upgrade, and runs the migrations after the services are up again. The guards of PeerTube and Pixelfed refuse a downgrade and dump the database before an upgrade. core/setups/nixos/postgresql.nix guards the major version of PostgreSQL and uses pg_upgrade.
  • Backup before the upgrade: core/components/shared/database-upgrade-backup.nix sends the dump to the restic repository <server>-upgrade in the backup bucket of the application. The repository keeps the snapshots of the 2 newest upgrades. Without an object store, the dump stays on the node.

Stores that change: the stores of the install flow, the database of the application (schema migrations), the version file of each guard on the node, and the restic repository of the upgrade backups.

Failure paths and rollback:

  • A guard that fails keeps the service down. deploy_verdict then fails the deploy.
  • The deploy method k8s sets a pod that does not become ready back to the previous image (kubectl rollout undo in core/effects/k8s/effect.nix) and fails the deploy. The state on the volume of the pod stays as it is. No other code path rolls back a deploy automatically.
  • A guard refuses a downgrade, thus a submit of the older revision does not go back after a migration of the data.
  • The way back is the dump from before the upgrade. The hosting provider guide gives the restore procedures.
  • A submit with apply_update set to false uses the module source of the last build that was deployed.

Planned:

  • Application versions that do not depend on the revision of the provider (issue 304).
  • Resource versions (issue 634).
  • An automatic rollback of a failed update, data included (issue 3052).
  • A guide for the operator through the options that an update adds or changes (issue 3054).

Manage users

There are two levels of users: the account of the operator in panel, and the users of each deployment. Each deployment has its own directory (lldap at users.<domain>) and its own identity provider (Authelia at auth.<domain>).

sequenceDiagram
    actor O as Operator
    actor U as User of an application
    participant P as panel
    participant IdP as Identity provider of the hosting provider
    participant L as lldap of the deployment
    participant Au as Authelia of the deployment
    participant K as OpenBao KV
    participant App as Application
    O->>P: log in
    P->>IdP: OpenID Connect authorization
    IdP-->>P: identity of the operator
    O->>L: add a user, put the user in a group
    U->>App: log in
    App->>Au: OpenID Connect authorization
    Au->>L: check the password and the groups
    Au-->>App: identity of the user
    L->>K: timer writes the members of the role groups
    App->>K: timer reads the members
    App->>App: set the role of each member

Code paths:

  • Log-in to panel: panel/src/panel/oidc.py uses OpenID Connect when OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET_FILE are all set (settings.py). If not, panel uses the log-in of Django. A wallet log-in (panel.wallet.WalletAuth) opens only an account that linked the wallet.
  • Directory: core/components/shared/lldap and core/components/shared/authelia. core/resources/ldap/lldap-provider.nix makes the first admin user.
  • Single sign-on: core/resources/oidc/authelia-oidc-provider.nix makes an OpenID Connect client for each application. allowedGroups becomes an Authelia policy that allows only the members of those groups. The application reads its client from the oidc contract (for example core/components/operator/mastodon/oidc.nix).
  • Groups to roles: core/resources/ldap/application-role-groups.nix and the role-groups.nix of each application declare the role groups, for example mastodon-owners, mastodon-admins and mastodon-moderators. core/resources/ldap/publish-roles.nix writes the members of the role groups to the OpenBao KV on a timer, only when they change. The ldap-roles.nix of each application reads the members on a timer and sets the roles in the application.
  • Legal roles gives the role of each group in each application. The hosting provider guide and the user guide of the panel give the procedure.

Stores that change: the database of lldap, the members entry in the OpenBao KV, and the accounts and roles in the database of each application. The account of the operator is in the database of panel.

Failure paths:

  • A role sync that fails does not fail a deploy. The timer tries again at its next interval.
  • A role change is not immediate: the publish and the sync each run on a timer.
  • Without the OpenBao backend for secrets, the role sync does nothing.
  • A user who is not in an allowed group cannot log in to the application.

Planned: api has no route, and panel has no view, that changes the users of a deployment. The operator uses the web interface of lldap. Issue 3055 asks for the decision if the panel must manage users.

Switch hosting provider

A switch has two parts: the export at the source provider, and the import at the target provider. The migration string moves between them. The operator carries the string.

sequenceDiagram
    actor O as Operator
    participant SA as api of the source
    participant SN as Nodes of the source
    participant S3 as Object store of the source
    participant R as Registrar
    participant TA as api of the target
    participant TC as core of the target
    participant TN as Nodes of the target
    O->>SA: GET /migration-string/{id}
    SA->>R: read the auth code, unlock the domain, remove the DNSSEC keys
    SA->>SN: stop the application services
    SN->>S3: restic snapshot of each application
    SA->>SA: mark the deployment as exported, submit suspend
    SA-->>O: migration string
    O->>TA: POST /migration-import/{id} with the string
    TA->>TA: check the module source, remove the provider settings of the source
    TA->>TC: dry evaluation
    TA->>TA: write the snapshot pointers and the auth code to OpenBao
    TA->>TC: run tf-run.sh
    TC->>R: transfer the domain with the auth code
    TC->>TC: hold the zone, wait for the delegation, publish the records
    TC->>TN: switch to the NixOS configuration
    TN->>S3: restore each application from its snapshot
    TC->>TC: delete the pointers and the auth code
    SA->>TN: zone transfer, copy the records to the old zone

Code paths:

  • Panel: static/migration_export.js calls GET /migration-string/{id} and shows the string. static/migration_import.js makes a deployment and sends the string to POST /migration-import/{id}.
  • Domain: migration.migration_export (api/src/fediversity_api/migration.py) first calls release_domain, which reads the auth code at the registrar, unlocks the domain and removes its DNSSEC keys. This occurs before the snapshot, thus a registrar that fails stops no service.
  • Export: migration_export then runs core/effects/migration/run.sh. This script stops the services of each node with migration-quiesce (core/components/shared/migration-quiesce.nix), then makes a snapshot of each backup unit. Then api marks the deployment as exported and submits the action suspend.
  • Import: migration.migration_import parses the string, removes the settings that belong to the source provider, and changes the configuration to the shape of the target. import_check.py evaluates the result with DRY_RUN=1 before api stores it. Then the import is the submit of the install flow.
  • Restore: migration_kv.py writes the snapshot pointers and the auth code to the OpenBao KV of the deployment. The OpenTofu graph gets only the snapshot references. core/effects/tf/incus/operator/restore-migration.sh reads the pointers, restores each unit, and deletes the pointers after the restore.
  • Domain transfer: registrar.tf gives the auth code to openprovider_domain.operator, then null_resource.drop_domain_transfer deletes the code from the KV.
  • DNS change: the deploy at the target holds the zone on the nameservers of the target (dns-lane-zone.tf). await_delegation waits until the parent zone delegates to them. Then the deploy publishes the records of the nodes.
  • Zone follow: when the source holds the zone at deSEC, zone_follow.py at the source copies the records of the target to the old zone until the old delegation is out of the caches of resolvers. It asks the nameservers of the target for a zone transfer that it signs with the transfer_key of the migration string.

The hosting provider guide gives the procedure of the export and of the restore. The contract templates give the switching clauses.

Stores that change:

Provider Store Change
Source Object store One new restic snapshot for each application.
Source Database of api The deployment is exported and suspended.
Source Registrar The domain is unlocked and has no DNSSEC keys.
Source Zone at deSEC The records follow the target for the cache time.
Target All stores of the install flow A new deployment.
Target OpenBao KV The pointers and the auth code, until the restore and the transfer end.
Target Databases and files of the applications The data of the source.
Target Registrar The domain is in the account of the target.

Trust between the two providers

  • The two providers have no direct connection for the import, and no common account. The operator carries the migration string. The string is a bearer capability: its holder can read all data of the deployment.
  • The target does not trust the configuration of the source. It validates the configuration against the schema, and accepts only a module source that is on its trusted-source allowlist.
  • The target removes the provider settings of the source from the string (operator_settings, RUNTIME_SETTING_KEYS).
  • The target reads the snapshots directly from the object store of the source, with the credentials of the string.
  • The target allows a zone transfer from the addresses in transfer_sources, to the holder of the transfer_key.
  • The string carries no registrar login.

Secrets that move

The migration string carries:

  • for each backup unit, the S3 endpoint, bucket, access key and secret key of the snapshot repository, the password of the repository, and the snapshot reference,
  • the keys that decrypt the data of the applications,
  • the auth code of the domain, when the source has the domain in its registrar account,
  • the TSIG key of the zone follow.

api does not keep the string, and does not write it to a log or to an audit event. At the target, the pointers and the auth code are in the OpenBao KV of the deployment, and not in the OpenTofu state or in the Windmill job. The deploy deletes them after their use. The threat model and data protection give the related risks and duties.

Downtime

The applications are down from the export until the target restores them. The export stops the services before the snapshot, thus no write is lost. The time has these parts: the snapshot, the transport of the string by the operator, the deploy at the target, the restore, and the DNS change. The code sets no limit on the full time, and the tree has no measured value. The limits that the code does set are:

  • the dry evaluation of the import: 1800 seconds, then build_timeout,
  • the cache time that the zone follow uses: at most 604800 seconds (TTL_CEILING in zone_follow.py).

To roll back, resume the source deployment: its data stays, and its services start again. After a domain transfer, the resume of the source fails.

Different versions at the two providers

  • The migration string must name its module source. An import without a module source is refused, because the revision of the target can be different from the revision of the source.
  • The target builds the schema of that module source, and applies its rename table to the configuration.
  • The target deploys with the version boundary of the update flow: the pinned revision for the application modules, and the newer revision for core/resources and core/effects.
  • The dry evaluation refuses a configuration that the pinned revision cannot deploy with the method and the environments of the target. This occurs before api stores the deployment.
  • A source node that runs an older revision does not give the keys of its applications. The export continues, and the string has no keys for that node.

Failure paths:

  • If a snapshot or the suspend fails, the export starts the services again and the deployment is not exported. If a snapshot fails, the export also locks the domain again.
  • If api cannot read the auth code, the export fails and does not change the source.
  • A second export of a suspended deployment gives conflict.
  • A string in an old form, or with a module source that is not on the allowlist, gives invalid_configuration.
  • If the restore fails, the deploy fails. The pointers stay in the KV, thus a new deploy restores again.
  • A domain transfer that the registry refuses shows as transfer_failed in GET /domains.

Planned:

  • Resource versions, which let the target reconcile a difference in the environments of the two providers (issue 634).
  • The domain calls need the registrar command of api, which exists for openprovider. A second registrar is planned (issue 1908).

Configuration data flow

This page tells how an operator configuration becomes a deployment. The data model and hosting resources pages are the option references for the same parts. The concepts page defines the terms.

From a configuration to a deployment

flowchart
  subgraph configuration
    application-config
  end
  subgraph resource[resource module]
    request
    policy
  end
  subgraph application[application module]
    application-options{{application-options}} --> application-config
    application-config --> config-mapping
    request{{request}} --> config-mapping
    config-mapping(config-mapping)
  end
  subgraph fediversity[fediversity setup]
    config-mapping -->|required resources| resource-mapping(resource-mapping)
    policy{{policy}} --> provider-config -->|available resources| resource-mapping -->|configuration| deployment{deployment}
  end
  maintainer((maintainer)) -->|curates| resource
  contributor((developer)) -->|curates| application
  operator((operator)) -->|enters| configuration
  hosting-provider((hosting provider)) -->|maintains| fediversity

In the diagram, a circle is an actor, a box with angled sides is a type, a rectangle is a value, a box with round corners is a function, and a diamond is a state. An arrow points to the part that uses the other part. An application module is a component that the operator can select.

  1. The operator selects the applications and sets their options in the panel. The panel form comes from frontendConfiguration, which has only the operator-facing components. The result is a configuration.
  2. Each enabled component maps its options to the resources that it needs. The implementation of a component, a config-mapping, returns one request for each resource, keyed by an instance name. For example, Forgejo requests a TLS certificate as "tls".ssl, its secrets as "age".secrets, and outbound mail as "forgejo-smtp".smtp.
  3. The hosting provider declares an environment. Its resources set the policy of each resource: which provider gives the resource, and with which limits.
  4. The implementation of the environment, a resource-mapping, collects the requests of all components. For each resource, the process function of the policy turns the requests into NixOS modules or OpenTofu resources.
  5. The result is a deployment for an effect, for example a set of Incus containers that OpenTofu creates and NixOS configures.

environments.<env>.deployment does all of these steps for one deployment.

Resources

A resource is the interface between a component and a hosting provider. The component sets the request, and the hosting provider sets the policy. The request/policy resource pattern on the data model page tells more, and the hosting resources page lists the request options of each resource.

A resource is not a contract of RFC 189. A resource usually uses one or more such contracts to give what a request asks for. The Contracts (RFC 189) page tells how.

Where the code is

Part Location
The data model core/lib/data-model.nix
The resources core/resources/
The components core/components/
The environments core/setups/hosts-common.nix
The effects core/effects/

Further reading

Contracts (RFC 189)

This page tells how the contracts of RFC 189 connect the software that needs a service to the software that gives it. A contract is the interface between a consumer module and a provider module of a service, for example smtp, postgresql, ssl or oidc.

A contract is not a part of the Fediversity data model. The data model has resources, the interface between a component and a hosting provider. A resource usually uses one or more contracts to give what a request asks for. The configuration data flow page tells how a configuration becomes a deployment through the resources.

For example, Forgejo asks the smtp contract for outbound mail with a sender address. The mail relay that provides the contract answers with a host, a port, a user name and a password file:

A contract has these parts, under config.contracts.<type>:

  • want.<consumer>.<instance>.request -- what a consumer asks for.
  • providers.<name> -- the providers that can answer the request.
  • defaultProvider -- the provider that answers when the consumer selects none. A want that no provider answers fails the evaluation.
  • providerRequests.<provider> -- the requests that a provider reads.
  • results.<consumer>.<instance> -- what the consumer reads back, for example a host, a port and a password file.

Each contract type defines the shape of its request and its result. The consumer and the provider agree only on these shapes, so a hosting provider can replace a provider without a change to the consumer. The contract reference lists the request and the result of each contract type, with its providers and an example.

Contracts across nodes

Each node of a deployment is a separate NixOS evaluation, so the nodes do not share config.contracts. When the consumer and the provider are on different nodes, crossNodeContract and crossNodeWant in core/lib/lib.nix evaluate the contract once, and give the result to each node. A contract that only gives coordinates, such as the address of a DNS server, does not need this. What a cross-node contract costs tells when to use one.

Where the code is

Part Location
The contracts machinery nix/contracts/lib/contracts/module.nix
The contract types nix/contracts/lib/contracts/definitions/
The cross-node helpers core/lib/lib.nix

The contracts guide of the vendored contracts tree tells how to write a consumer, a provider and a new contract type.

Further reading

Credentials register

This part lists the credentials of the system: where each one is, what makes it, how long it lives, how it changes, and what a change interrupts. The page gives names and locations only. The components that can read each credential are in the access model.

The word "Open" in a cell means: the code and the documentation give no procedure. The page gives a period or a procedure only when the code or a document holds it.

How the register is made

The tables that have the label "generated" come from the code at each build of the documentation (core/docs/md/technical-design/credentials.nix). Each generated table has two inputs:

  • The names, which the build reads from the code: the realised configuration of the nodes with the groups of core/testers/groups.nix, the file core/resources/secrets/secrets.nix, and the workflows in .forgejo/workflows/.
  • The facts that the code does not hold (the procedure and what it interrupts), which a person writes in credentials.nix.

The build compares the two inputs. The check core-docs fails when a name has no facts, or when facts have no name. The other tables are written by hand in this file.

The specification of the secret rotation gives the requirements for rotation. Its table in "Key Entities" adds, for each secret, the backups that depend on the secret and how the consumers get a new value. This page is the source for the list of the credentials and for the state of their rotation.

Credentials that OpenBao rotates

OpenBao makes these credentials and changes them on a schedule. No person and no deploy writes them. A consumer reads the current value through its OpenBao agent. The lifetime of a value is the rotation period. The code does not state what a rotation interrupts: the column "Consumer refresh" of the specification gives how each consumer gets the new value.

Generated:

Credential Path that the consumer reads in OpenBao Rotation period
PostgreSQL role of the database hosting-provider-authelia postgresql/static-creds/hosting-provider-authelia (rotating) 24h
PostgreSQL role of the database hosting-provider-lldap postgresql/static-creds/hosting-provider-lldap (rotating) 24h
PostgreSQL role of the database hosting-provider-netbox postgresql/static-creds/hosting-provider-netbox (rotating) 24h
PostgreSQL role of the database hosting-provider-windmill postgresql/static-creds/hosting-provider-windmill (rotating) 24h
PostgreSQL role of the database operator-authelia postgresql/static-creds/operator-authelia (rotating) 24h
PostgreSQL role of the database operator-lldap postgresql/static-creds/operator-lldap (rotating) 24h
PostgreSQL role of the database operator-mastodon postgresql/static-creds/operator-mastodon (rotating) 24h
PostgreSQL role of the database operator-peertube postgresql/static-creds/operator-peertube (rotating) 24h
PostgreSQL role of the database operator-pixelfed postgresql/static-creds/operator-pixelfed (rotating) 24h
LDAP account authelia-bind, read by the node authelia ldap/static-cred/authelia-bind 24h
LDAP account authelia-bind, read by the node operator-authelia ldap-operator/_shared/static-cred/authelia-bind 24h
LDAP account authelia-bind of each deployment ldap-operator/<namespace>/static-cred/authelia-bind 24h
Valkey user app of the namespace hosting-provider-authelia, read by the node authelia database/static-creds/hosting-provider-authelia See the text below
Valkey user app of the namespace hosting-provider-valkey, read by the node valkey database/static-creds/hosting-provider-valkey See the text below
Valkey user app of the namespace operator-authelia, read by the node operator-authelia database/static-creds/operator-authelia See the text below
Valkey user app of the namespace operator-mastodon, read by the node operator-mastodon database/static-creds/operator-mastodon See the text below
Valkey user app of the namespace operator-peertube, read by the node operator-peertube database/static-creds/operator-peertube See the text below
Valkey user app of the namespace operator-pixelfed, read by the node operator-pixelfed database/static-creds/operator-pixelfed See the text below
Valkey user app of the namespace operator-valkey, read by the node operator-valkey database/static-creds/operator-valkey See the text below

The rotation period of a Valkey namespace is not an option. It is 24 hours in core/components/hosting-provider/openbao/valkey-engine.nix. The engine rotates the user app of a namespace. It connects as the user default, and it does not rotate that user: the password of default is the generated secret valkey-<namespace> in the next section. The LDAP engine rotates only the accounts in the table. It connects with the management account of lldap, and it does not rotate that account (the instances ldap-bind-* in the next section).

Generated secrets in the KV of OpenBao

A node declares each secret that it needs as a generateFiles request. These facts apply to each row of the table:

  • Where: the KV path is <prefix>/secret/<instance>/<file> in the mount secret. The prefix is vars for the hosting provider and vars/operator-deployments/<namespace> for a deployment.
  • Maker: the script of the request makes the value at the first deploy of the node (nix/contracts/nixos/modules/services/security/vars-openbao.nix).
  • Lifetime: the code sets no end. A deploy keeps a value that is in KV.
  • Change: a deploy with VARS_FORCE_UPLOAD=<instance> or VARS_FORCE_UPLOAD=<instance>/<file>, or the command upload-vars-openbao-force on the node, makes a new value and writes it. This is a write, not a rotation procedure: it does not tell what the consumers do with the new value. Thus a row without a procedure for its consumers has "Open".
  • For some keys OpenBao keeps all versions, because a backup from before a change needs the old key. The column "KV keeps all versions" shows them.

The table shows the nodes of the hosting provider group and of one operator group of the testers. A file that is not secret (for example a public key) is not in the table.

Generated:

Credential Instances Files Nodes that request it KV keeps all versions Rotation What a rotation interrupts
Password of the restic repositories of an application authelia-backups-restic, bind-backups-restic, grafana-backups-restic, hosting-provider-authelia-cache-backups-restic, hosting-provider-valkey-cache-backups-restic, lldap-backups-restic, mastodon-backups-restic, netbox-backups-restic, operator-authelia-cache-backups-restic, operator-mastodon-cache-backups-restic, operator-peertube-cache-backups-restic, operator-pixelfed-cache-backups-restic, operator-valkey-cache-backups-restic, peertube-backups-restic, pixelfed-backups-restic, postgresql-backups-restic, windmill-backups-restic password hosting-provider/authelia, hosting-provider/bind, hosting-provider/lldap, hosting-provider/netbox, hosting-provider/postgresql, hosting-provider/telemetry, hosting-provider/valkey, hosting-provider/windmill, operator/authelia, operator/lldap, operator/mastodon, operator/peertube, operator/pixelfed, operator/valkey no By hand: Password of the restic repositories The procedure restarts openbao-agent-default. The repositories accept the old and the new password during the change
Password and salt of the encrypted copy of the media buckets mastodon-media-backup, peertube-media-backup, pixelfed-media-backup password, salt operator/mastodon, operator/peertube, operator/pixelfed no Open Open
Secret key of an S3 key of garage authelia-backups-s3, bind-backups-s3, grafana-backups-s3, hosting-provider-authelia-cache-backups-s3, hosting-provider-valkey-cache-backups-s3, lldap-backups-s3, netbox-backups-s3, openbao-backups-s3, postgresql-backups-s3, telemetry-s3, terraform-backend-backups-s3, windmill-backups-s3 secretKey hosting-provider/garage, hosting-provider/telemetry no Open Open
Admin token and RPC secret of garage garage-admin adminToken, rpcSecret hosting-provider/garage no Open Open
Storage encryption key of authelia authelia-oidc, operator-authelia-oidc storageEncryptionKey hosting-provider/authelia, operator/authelia yes By hand: Authelia storage encryption key Authelia is stopped during the change
OIDC and JWT secrets of authelia authelia-oidc, operator-authelia-oidc jwtSecret, oidcClientSecret, oidcClientSecretDigest, oidcHmacSecret, oidcIssuerPrivateKey hosting-provider/authelia, operator/authelia no Open Open
Session secret of authelia authelia-session sessionSecret hosting-provider/authelia, operator/authelia no Open Open
Password of the user operator of the state backend terraform-backend-operator password hosting-provider/terraform-backend no Open Open
TSIG keys of the name server acme-dns, octodns, operator-dns tsigSecret hosting-provider/bind no Open Open
DNSSEC key of the name server dnssec key, private, state hosting-provider/bind no Open Open
Password of the management account of lldap ldap-bind-hosting-provider, ldap-bind-operator bindPassword hosting-provider/lldap, operator/lldap no Open Open
JWT secret of lldap ldap-jwt-hosting-provider, ldap-jwt-operator jwtSecret hosting-provider/lldap, operator/lldap no Open Open
Key seed of lldap ldap-key-hosting-provider, ldap-key-operator keySeedEnv hosting-provider/lldap, operator/lldap yes Never without loss: Keys that cannot change without data loss lldap does not start with a new seed. With LLDAP_FORCE_UPDATE_PRIVATE_KEY=true it starts, and all users must set a new password
Secret key and token peppers of NetBox netbox apiTokenPeppersFile, secretKeyFile hosting-provider/netbox no Open Open
Password of the PostgreSQL administrator postgresql-admin password hosting-provider/postgresql no Open Open
Submission passwords of the mail server operator-smtp, smtp autheliaPassword, lldapPassword, mastodonPassword, netboxPassword, peertubePassword, pixelfedPassword, telemetryPassword, windmillPassword hosting-provider/smtp, operator/smtp no Open Open
DKIM key of the mail server operator-smtp-dkim, smtp-dkim fediversity.test_fediversity.key hosting-provider/smtp, operator/smtp no Open Open
Secret key of the Grafana of the hosting provider telemetry-grafana secretKey hosting-provider/telemetry yes By hand: Grafana secretKey Grafana restarts
Old secret keys of the Grafana of the hosting provider telemetry-grafana-previous secretKeys hosting-provider/telemetry no Written and emptied in the procedure of the row above See the row above
Admin password of the Grafana of the hosting provider telemetry-grafana adminPassword hosting-provider/telemetry no Open Open
Credentials of the telemetry and audit endpoints audit-ingest-api, metrics-query, node-telemetry, telemetry-ingest authHeader, htpasswd, password hosting-provider/telemetry, operator/grafana no Open Open
Secret key of the Grafana of a deployment grafana-secret secretKey operator/grafana no Open Open
Encryption key of the state backend terraform-backend kmsKey hosting-provider/terraform-backend no Open Open
Password of the Valkey user default of a namespace valkey-hosting-provider-authelia, valkey-hosting-provider-valkey, valkey-operator-authelia, valkey-operator-mastodon, valkey-operator-peertube, valkey-operator-pixelfed, valkey-operator-valkey password hosting-provider/valkey, operator/valkey no Open Open
Password of the superadmin of windmill windmill-superadmin adminPassword hosting-provider/windmill no Open Open
Active Record encryption keys of Mastodon mastodon-keys activeRecordEncryptionDeterministicKey, activeRecordEncryptionKeyDerivationSalt, activeRecordEncryptionPrimaryKey operator/mastodon yes Never without loss: Keys that cannot change without data loss The private keys of the accounts and the two-factor secrets do not decrypt
SECRET_KEY_BASE and VAPID key of Mastodon mastodon-keys secretKeyBase, vapidPrivateKey operator/mastodon yes By hand, with the effects that Keys that cannot change without data loss gives No data is lost. All users must log in again, the links in password reset and confirmation emails stop, and the push subscriptions stop until the clients subscribe again
Secret of PeerTube peertube-secret secret operator/peertube yes Never without loss: Keys that cannot change without data loss The two-factor authentication of all users that use it fails
APP_KEY of Pixelfed pixelfed-app appKey operator/pixelfed yes By hand: Pixelfed APP_KEY Pixelfed restarts two times. A user that did not use pixelfed during the change must log in again
Old APP_KEY values of Pixelfed pixelfed-app-previous appKeys operator/pixelfed no Written and emptied in the procedure of the row above See the row above
Passport key of Pixelfed pixelfed-passport privateKey operator/pixelfed no Open Open

The change of an application encryption key has its own work item (fediversity/fediversity issue 2433).

Other secrets in the KV of OpenBao

No generateFiles request of a node of core makes these entries. The api node and the panel node are not in the setups that the build of this page reads, thus their rows are written by hand.

Credential Access Where Maker Lifetime Rotation What a rotation interrupts
Key of the target of the external copy of the backups The garage node vars/secret/backup-external-copy/<name> A person writes it with bao kv put (hosting provider guide) The code sets no end Open Open
Submission password for the upstream mail relay The node that relays the mail vars/secret/smarthost-upstream/<name> (core/resources/smtp/maddy-provider.nix) Not made by a node The code sets no end Open Open
Token of the api for windmill (WINDMILL_TOKEN) The api node Instance windmill, file apiToken The windmill node, at run time (core/components/hosting-provider/windmill/windmill.nix) The code sets no end Open Open
Service token between the panel and the api (API_SERVICE_TOKEN) The panel node and the api node Instance panel-service, file token (panel/nix/component/panel.nix) The panel node, at its first deploy The code sets no end Open Open
Callback token of the authorization check (AUTHZ_CALLBACK_TOKEN) The panel node and the api node Instance panel-authz, file token (panel/nix/component/panel.nix, api/nix/component/authz-token.nix) The panel node, at its first deploy The code sets no end Open Open
Secret key of the panel The panel node Instance panel, file secret_key The panel node, at its first deploy The code sets no end Open Open
API key of centrifugo The api node Instance api-centrifugo, file apiKey (api/nix/component/default.nix) The api node, at its first deploy The code sets no end Open Open
Credentials that the api deploys with (state backend password, zones token) The api node Instance deployer (core/components/hosting-provider/deploy-credentials.nix) The CD publishes them from the secrets of the forge The code sets no end Open Open
Tokens that a node writes at run time The nodes that the access model gives Instances deploy-tokens (incus), netbox (NetBox) and metrics (Mastodon, PeerTube, Pixelfed) The node that owns the instance, at run time The code sets no end Open Open

Credentials of OpenBao

Credential Access Where Maker Lifetime Rotation What a rotation interrupts
Scoped tokens (list below) The units of the openbao node /var/lib/openbao-init/tokens/<name> on the openbao node openbao-init Periodic token with a period of 768h Automatic: openbao-tokens-renew renews each token daily. openbao-init makes a token again when it is missing or not valid Open
Root token An administrator on the openbao node Not stored: openbao-init revokes its root token when it exits openbao-init, or the command openbao-root-token Until openbao-init exits Does not apply Does not apply
Unseal key The openbao node, and the backup recipients Sealed to the TPM of the node. A copy is encrypted to fediversity.bootstrapBackupRecipients openbao-init The code sets no end Open. When the recipients change, openbao-init encrypts the same key again (core/components/hosting-provider/openbao/escrow.nix) Open
CA of the PKI engine OpenBao The PKI mount of OpenBao (core/components/hosting-provider/openbao/pki-init.nix) The openbao node (pki-init.nix) maxLeaseTtl, 10 years by default. A certificate that it signs lives 30 days at most (roleMaxTtl) Open Open
CA of the admin SSH login OpenBao signs with it. Each node trusts its public key The engine ssh-admin of OpenBao. The forge variable ADMIN_SSH_CA_KEYS holds the public keys The openbao node (ssh-certificates.nix) The code sets no end. A certificate that it signs lives 8 hours (certificateTtl) By hand: Rotate the CA The procedure trusts the two keys until the old certificates end

Generated, the names of the scoped tokens:

database-init, deployer-kv-prune, kv-key-metadata, ldap-engine, oidc-auth, operator-deployer-token, pki-init, pki-leaf, snapshot, spire-auth, spire-cert-auth, ssh-certificates, valkey-engine

Credentials of SPIRE

Each node logs in to OpenBao with an identity that SPIRE gives. Without an upstream root, the SPIRE server signs its own CAs, and each CA lives for about one day. With the upstream root, the root lives 10 years and each CA lives 24 hours. The private key of the root is the forge secret SPIRE_ROOT_KEY in the section about the forge. The specification of spire gives the custody and the rotation of the root in "The upstream root".

Keys of persons

Credential Access Where Maker Lifetime Rotation What a rotation interrupts
SSH certificate of an administrator The administrator With the administrator The engine ssh-admin 8 hours Automatic: the administrator gets a new certificate (Log in with a certificate) Does not apply
SSH key of a contributor The contributor, who has root on the machines of the development infrastructure. The key is also a recipient of the agenix secrets The public half is in core/keys/contributors/ The contributor The code sets no end By hand, no schedule: core/keys/README.md, "Adding a contributor" and "Removing a contributor". A removal needs a rekey, a deploy, and a rotation of the secrets Open
Keys and password hash in ADMIN_EMERGENCY The holder of the offline emergency key, as the account emergency (wheel) on each node. Only for when no certificate is available The forge variable ADMIN_EMERGENCY (core/setups/nixos/users.nix); the private key offline A person The code sets no end Open Open
SSH key of the CD (cd) The CD jobs. The key is also a recipient of the agenix secrets The public half is core/keys/cd-ssh-key.pub A person The code sets no end Open Open
Host key of a system The system. The key is a recipient of the agenix secrets that it reads The public half is in core/keys/systems/ Open The code sets no end Open Open

Secrets of the development infrastructure

The files core/resources/secrets/<name>.age hold the secrets of the forge and its runners. Each file is encrypted to the recipients that core/resources/secrets/secrets.nix gives. A person makes each value, and the code sets no end for it. A change of the recipients is a rekey (core/resources/secrets/README.md, "Rekeying"): it needs a person who is a recipient now, and it does not change the value.

Generated:

File Credential Rotation What a rotation interrupts
anubis-forgejo-key.age Key of the anubis in front of the forge By hand: core/resources/secrets/README.md, "anubis-forgejo-key" Each visitor of the forge solves one challenge again
forgejo-database-password.age Password of the database user of the forge By hand: core/resources/secrets/README.md, "forgejo-database-password" New database connections of the forge fail between the change in the database and the restart of forgejo
forgejo-email-password.age Password of the mail account of the forge By hand: core/resources/secrets/README.md, "forgejo-email-password" Open
forgejo-runner-tokens-dev2.age Registration tokens of the runners of one machine By hand: core/resources/secrets/README.md, "forgejo-runner-tokens-<machine>" Open
forgejo-runner-tokens-dev3.age Registration tokens of the runners of one machine By hand: core/resources/secrets/README.md, "forgejo-runner-tokens-<machine>" Open
forgejo-runner-tokens-dev4.age Registration tokens of the runners of one machine By hand: core/resources/secrets/README.md, "forgejo-runner-tokens-<machine>" Open
forgejo-runner-tokens-fediversity-ci.age Registration tokens of the runners of one machine By hand: core/resources/secrets/README.md, "forgejo-runner-tokens-<machine>" Open
security-pgp-key.age OpenPGP key that reporters of vulnerabilities encrypt to By hand: core/resources/secrets/README.md, "security-pgp-key and security-pgp-passphrase" Each reader of the reports imports the key again
security-pgp-passphrase.age Passphrase of the OpenPGP key By hand: core/resources/secrets/README.md, "security-pgp-key and security-pgp-passphrase" See the row above

Secrets of the forge

The CD jobs and the other workflows read these secrets from the Forgejo Actions secrets of the repository. A person with the rights to set secrets on the forge writes each one. The code sets no end for a value, apart from the incus client certificate: the command in notes/cd-credentials.md makes it for 365 days. .forgejo/workflows/cd-env.example.sh gives the same names for a deploy from a workstation. The forge variable TF_BACKEND_HTPASSWD holds one line for each user of the state backend (cd, operator, and workstation-<person>), with the hash of the password only. The rotation of the password of a workstation-<person> user is Open.

Generated:

Secrets Credential Rotation What a rotation interrupts
SSH_PRIVATE_KEY SSH key that the CD jobs use to reach the nodes Open Open
INCUS_CLIENT_CRT, INCUS_CLIENT_KEY Client certificate of the CD for the incus API By hand: notes/cd-credentials.md, "Rotating it" The procedure adds the new certificate before it removes the old one
TF_HTTP_PASSWORD Password of the user cd of the state backend. The variable TF_BACKEND_HTPASSWD holds its hash. The state backend node makes the password of the user operator Open Open
SMARTHOST_UPSTREAM_PASSWORD Password of the account of the upstream smarthost. The deploy of the hosting provider writes it to the KV entry that the relays read Open Open
DNS_API_TOKEN, OPERATOR_DNS_API_TOKEN, OPERATOR_ZONES_MANAGER_TOKEN Tokens for the API of the DNS provider Open Open
DNS_TSIG_SECRET, DNS_SECONDARIES TSIG keys: the key of a zone that the name server holds, and the transfer keys of the secondaries Open Open
NETBOX_API_TOKEN Token for the API of NetBox Open Open
INWX_USERNAME, INWX_PASSWORD, INWX_SHARED_SECRET, OPENPROVIDER_USERNAME, OPENPROVIDER_PASSWORD, PORKBUN_API_KEY, PORKBUN_SECRET_KEY Credentials of the registrars Open Open
FORGEJO_BACKUP_RESTIC_PASSWORD, FORGEJO_BACKUP_S3_ACCESS_KEY, FORGEJO_BACKUP_S3_SECRET_KEY Password and S3 key of the offsite backup of the forge By hand: set the value, then start the workflow cd-dev-forgejo (notes/cd-credentials.md) Open
RELEASE_SIGNING_KEY SSH key that signs the release tags By hand: Set up the signing key The file .forgejo/workflows/release-allowed-signers keeps the old key for the old tags
SPIRE_ROOT_KEY Private key of the upstream root of SPIRE. The variable SPIRE_ROOT_CERT holds the certificate By hand: the specification of spire, "The upstream root" Open
SBOM_PACKAGE_TOKEN Token that publishes the SBOM package (SBOM) Open Open
BOT_PAT, DEPLOY_KEY Token and SSH key that the automation workflows use on the forge (update.yaml, registry-terms.yaml, vulnerability-scan.yaml) Open Open

The workflows also read these names as secrets. They give an address, a name or a selector, not a credential. Generated:

ADMIN_SOURCES, BIND2_IPV4, BIND2_IPV6, BIND_IPV4, BIND_IPV6, CLUSTER_SOURCES, DNS_APEX_NAMESERVERS, DNS_DOMAIN, DNS_PROVIDER, DNS_SECONDARIES_BY_ADDRESS, DNS_SERVER_URL, NETBOX_CLUSTER_NAME, NETBOX_CUSTOM_FIELDS, NETBOX_PREFIX_SELECTOR, NETBOX_RANGE_SELECTOR, NETBOX_SERVER_URL, NETBOX_TAGS, NETBOX_TENANT, OPERATOR_DNS_PROVIDER, OPERATOR_DNS_SERVER_URL, OPERATOR_NETBOX_RANGE_SELECTOR

Signing key of the bridge cache

The incus host signs the paths that its binary cache serves to its containers.

  • Where: /var/lib/incus-bridge-cache/ on the host. The key does not go out of the host.
  • Maker: the host.
  • Lifetime: fediversity.incus.bridgeCacheKey.rotationDays, 90 days by default.
  • Rotation: automatic. The timer incus-bridge-cache-key-rotate runs one time each day. The containers trust the previous key for fediversity.incus.bridgeCacheKey.overlapDays, 30 days by default.
  • What a rotation interrupts: nix-serve restarts.

The procedure after a compromise is in Signing key of the bridge cache.

Data subject requests

A person can ask to see, to correct, to erase or to get a copy of the personal data that a deployment or a hosting provider holds about them. This page tells, for each store that holds personal data, which party answers such a request, and which mechanism the code in this repository gives for it. Where the code gives no mechanism, the page says "no mechanism found".

This page is not legal advice. It does not tell if a request is valid, which data is in its scope, or how much time the answer can take. It tells only what the code can do.

The stores and their personal data are in Data protection. The part state-register has the full list of the state that the system stores. This page does not repeat these lists.

Who answers a request

The party that controls the data answers the request. Data protection gives the roles:

  • The operator is the controller of the data of its deployment: the accounts and the content of its applications, and the users of the lldap and of the Authelia of its group.
  • The hosting provider is the controller of its own data: the accounts of the panel, the users of the lldap of the hosting-provider group, the audit records, and the accounts of Grafana, NetBox and Windmill.
  • The hosting provider is the processor of the operator for the data of a deployment. For some stores of a deployment, only the hosting provider has a tool, for example a command on a node or a backup. Then the operator answers the request, and the hosting provider does the step.
flowchart TD
  request["Request of a person"] --> which{"Which data?"}
  which -->|"Data of a deployment"| operator["Operator answers"]
  which -->|"Data of the hosting provider"| provider["Hosting provider answers"]
  operator --> app["Functions of the application"]
  operator --> lldap["lldap of the group"]
  operator --> config["Configuration in the panel, for example deleteData"]
  operator -->|"asks for a step on a node, a log or a backup"| provider
  provider --> panel["Panel account page and admin pages"]
  provider --> api["Routes of api/"]
  provider --> node["Commands on a node, logs, backups"]

Mechanisms that the code gives

Mechanism Who uses it What it does
The account page of the panel The account /account/ shows the user name and the email address of the account. /account/history/ shows the audit events of the account (AccountDetail and AccountHistory in panel/src/panel/views.py).
The admin pages of the panel A panel administrator /admin/ shows, changes and removes an account (panel/src/panel/admin.py). Before a removal, the panel queues the destroy of each deployment of the account through api/. If api/ does not queue a destroy, the removal does not occur (_destroy_deployments in panel/src/panel/models.py).
GET /audit-events of api/ The party that the authorization callback permits It gives the audit events of one actor on all deployments (api/src/fediversity_api/routes/audit.py). With the panel, an account reads its own events, and a superuser reads the events of each account (reads_all_history in panel/src/panel/views.py).
/domain_contacts/{contact_id} of api/ The operator, through the Domain contact form of the panel GET gives the contact from the state, and PUT makes the contact at the registrar equal to the body (api/src/fediversity_api/routes/domains.py). DELETE deletes the customer at the registrar and the state, and refuses while a domain of the registrar account names the contact.
DELETE /deployments/{id} of api/ The operator, through the panel It destroys a deployment with all its data. See Know what a destroy keeps.
deleteData of an application The operator, in the configuration of the deployment With enable false and deleteData true, the next deployment deletes the database, the buckets, the generated secrets and the cache of the application (core/components/sharedOptions.nix). It deletes the data of all users of the application, not of one person.
The web UI of lldap A member of lldap_admin of the group lldap serves its web UI at https://users.<domain>/ (the label is in core/components/shared/lldap/ui-subdomain.nix). A member of lldap_admin manages each user of the group there. Without lldap_admin, the web UI shows a user only their own account (core/resources/ldap/lldap-provider.nix). The management functions are functions of lldap, which is not in this repository.
media-backup-<app> erase <bucket> <key> A person with a shell on the node of the application It removes one object from the media backup and from all of its history (core/resources/garage/media-backup.sh). No route of api/ and no page of the panel runs this command.
Retention No person The logs, the audit events, the jobs of Windmill and the backups expire after a period. See Know which personal data the logs keep and Know how long the backups keep personal data.

The panel and api/ have no route that finds, changes, erases or exports the data of one user of an application. The functions of the application do this work. This repository does not contain the code of the applications, thus this page does not describe these functions. Find them in the documentation of the application version that the deployment runs.

Data of a deployment

The operator answers a request about this data.

Store Access Rectification Erasure Export for one person
The database, the buckets and the local files of Mastodon, Pixelfed or PeerTube The functions of the application, by the user or by an administrator of the application. The code adds no mechanism. The functions of the application. The code adds no mechanism. The account deletion of the application. The operator settings ACCOUNT_DELETION and ACCOUNT_DELETE_AFTER of Pixelfed apply to it. deleteData erases the data of all users of the application. PeerTube: the operator can set export.users.enabled in settings (core/components/operator/peertube/default.nix). The code sets no value. Mastodon and Pixelfed: the functions of the application. The code adds no mechanism.
The cache of an application (the valkey of the group, or a local redis) No mechanism found. No mechanism found. No mechanism found for one person. deleteData deletes the cache of the application as a whole. No mechanism found.
The users of the lldap of the group The user reads their own account at https://users.<domain>/. A member of lldap_admin reads each user there. A member of lldap_admin changes the user there. A member of lldap_admin deletes the user there. No mechanism found.
The database of the Authelia of the group (consents, second factors, OIDC state) No mechanism found. No mechanism found. No mechanism found. No mechanism found.
The mail queue and the journal of the SMTP node of the group No mechanism found. No mechanism found. No mechanism found. The journal expires. No mechanism found.
The journal of the nodes of the deployment, and its copy in Loki No mechanism found for one person. The lines of one deployment have the label deployment_id in Loki. No mechanism found. No mechanism found for one person. The lines expire. No mechanism found.
The media backup of an application No mechanism found. A backup is only for a restore, see Erase personal data. No mechanism found. media-backup-<app> erase <bucket> <key> on the node, for each object. The hosting provider does this step. No mechanism found.
The restic backups (the databases, the local files, the caches) No mechanism found. No mechanism found. No mechanism for one record. An erased record stays until the backup expires, see Erase personal data. restic forget and prune remove whole snapshots before then. No mechanism found.
The *-backups buckets that a destroy keeps No mechanism found. No mechanism found. A timer deletes the bucket at the end of the grace period. An expires_at in the retention record deletes it earlier. No mechanism found.

After a restore from a backup that is older than an erasure, the erasure must be done again. The operation "Record an erasure" of an application keeps the time and the reference of each erasure on the node, and a restore from an older snapshot lists them until "Confirm the erasures". See Do the erasures again after a restore.

The export of a deployment for a migration gives all the data of the deployment to a different hosting provider, and stops its applications. It is not an export for one person. See Export a deployment for a migration.

Data of the hosting provider

The hosting provider answers a request about this data. The persons are the staff of the operators and the staff of the hosting provider.

Store Access Rectification Erasure Export for one person
The account of the panel The account reads /account/. A panel administrator reads the account at /admin/. A panel administrator changes the account at /admin/. The account page has no form for the user name or the email address. A panel administrator removes the account at /admin/. The removal also removes the deployments of the account, after the panel queued their destroy. The panel has no page where an account removes itself. No mechanism found.
The deployments of an account and their configuration, in the panel and in api/ The operator reads them in the panel. The operator changes the configuration in the panel. The operator deletes the deployment. The panel keeps the row of a deleted deployment (deleted_at in panel/src/panel/models.py) until the account is removed. No mechanism found.
The audit_event table of api/ /account/history/ of the panel, or GET /audit-events. No mechanism found. No mechanism found for one person. api/ removes the events that are older than its retention. No mechanism found.
The audit lines in the tenant audit of Loki No mechanism found for one person. No mechanism found. No mechanism found. This tenant refuses the deletion API of Loki (deletion_mode = "disabled" in core/components/hosting-provider/telemetry.nix). The lines expire. No mechanism found.
The domain contact of an operator (the OpenTofu state and the registrar) The operator reads it at Domain contact on the account page, through GET /domain_contacts/{contact_id}. The operator changes it in the same form, through PUT. A change of a name makes a new customer at the registrar. The call deletes the old customer, unless a domain names it. The operator erases it with "Erase contact" in the same form, through DELETE. The call refuses while a domain of the registrar account names the contact. The removal of the panel account erases it also. No mechanism found.
The users of the lldap of the hosting-provider group The user reads their own account at https://users.<domain>/. A member of lldap_admin reads each user there. A member of lldap_admin changes the user there. A member of lldap_admin deletes the user there. No mechanism found.
The database of the Authelia of the hosting-provider group No mechanism found. No mechanism found. No mechanism found. No mechanism found.
The accounts of Grafana, NetBox and Windmill The administration pages of each service. The code adds no mechanism. The administration pages of each service. The code adds no mechanism. The administration pages of each service. The code adds no mechanism. No mechanism found.
The jobs of Windmill No mechanism found for one person. No mechanism found. No mechanism found for one person. Windmill removes a completed job after services.fediversity-windmill.jobRetentionDays. No mechanism found.
The journal, Loki, the nginx access log and the audit lines of OpenBao No mechanism found for one person. No mechanism found. No mechanism found for one person. The lines expire. No mechanism found.
The backups of the databases of these services No mechanism found. No mechanism found. No mechanism found. An erased record stays until the backup expires. No mechanism found.

The removal of a panel account first erases the domain contact of the account. A contact that a domain of the registrar account names stays, and the panel writes a warning to its log. The removal does not remove the audit events of its actor or the user in lldap. Each of these is a different store with its own row in this table.

Where no mechanism exists

The code gives no mechanism for these requests:

  • An export of the data of one person, for all stores but the accounts of the applications. The export of a deployment is for the deployment as a whole (#3058).
  • Access, rectification or erasure for one person in the logs, the telemetry, the jobs of Windmill and the audit records. Only the retention removes this data. The account history of the panel gives access to the audit events of one actor (#3062).
  • Access, rectification or erasure in the database of Authelia and in the caches of the applications (#3061).
  • Erasure of one record from a restic backup before it expires. The procedure removes whole snapshots (#3060).
  • A page where a panel account removes itself, or changes its user name or email address (#3056).
  • One command or one page that finds all the data of one person in all stores. A party that answers a request goes to each store (#3057).

#2194 tracks the compliance needs of customers in the EU, and these gaps belong to it.

Deployment topology

This page shows which nodes a deployment has, where each node runs, and which networks connect the nodes. It also shows where the trust boundaries of the threat model are in that topology. The glossary defines the terms.

The build makes the diagrams and the tables of this page from the code: the group definitions (core/setups/groups), the components (core/components), the constants of the bridge (core/components/hosting-provider/incus-bridge.nix) and the attribute families of core (core/render/catalog.nix). The tables show the groups with the example apex example.org and with no settings from the environment.

Groups and nodes

A group is a set of machines that one deployment makes or changes. core/setups/groups/default.nix defines four groups:

  • hosting-provider has the shared services of a hosting provider.
  • operator has the applications of one operator and the services that only that operator uses. Each deployment of an operator is one instance of this group.
  • hypervisor has the host that runs the Incus daemon. It is a group of its own because it hosts the instances that the deployment of the hosting-provider group makes.
  • dev has the forge and the runners of the continuous integration.

Each group has two rosters of machines, published and internal. In the operator group, the published roster has the nodes of the applications and the internal roster has the nodes of the services that the applications use. Each node runs one component. In the tables, the column "Application" tells if an operator can enable or disable the component, and the column "Provides" gives the resources that the component supplies to other components (wiring.provides in core/components/wiringProperties.nix).

Group dev

Node Roster Component Application Provides
dev2 published forgejo-runner no --
dev3 published forgejo-runner no --
dev4 published forgejo-runner no --
fediversity-ci published forgejo-runner no --
forgejo published forgejo no --

Group hosting-provider

Node Roster Component Application Provides
authelia published authelia no oidc
bind published bind no --
garage published garage no objectStore
lldap published lldap no ldap
netbox published netbox no netbox
openbao published openbao no varsBackend, pki
otel-collector published otel-collector no otel
overview published overview no uiIndex
postgresql published postgresql no postgresql
smtp published smtp no smtp
spire published spire no spiffe
telemetry published telemetry no otelBackend
terraform-backend published terraform-backend no tfState
valkey published valkey no redis
windmill published windmill no --

Group hypervisor

Node Roster Component Application Provides
incus published incus no instances

Group operator

Node Roster Component Application Provides
authelia internal authelia no oidc
grafana internal grafana no --
lldap internal lldap no ldap
smtp internal smtp no smtp
valkey internal valkey no redis
mastodon published mastodon yes --
peertube published peertube yes --
pixelfed published pixelfed yes --

Notes for the tables:

  • The nodes api and panel are not in the tables. The leaves add them to the published roster of the hosting-provider group (api/default.nix, panel/default.nix).
  • The hosting-provider group has the node bind2 only when the environment gives its address (core/setups/groups/hosting-provider.nix).
  • The operator group has no database node and no object store node. Its applications use the postgresql and garage nodes of the hosting provider (core/setups/groups/operator.nix).
  • The machine incus of the hypervisor group and the machines dev2, dev3, dev4 and fediversity-ci of the dev group each have their own NixOS configuration in core/setups/hosts/.

Deployment methods

A deployment method decides how the nodes of a group come to exist and how they get their configuration. Flake attributes lists all methods and their commands. The table shows the methods that each group has.

Group Deployment methods
dev k8s, node-effects, ssh, tf, tf-incus
hosting-provider k8s, node-effects, ssh, tf, tf-incus
hypervisor k8s, node-effects, ssh, tf, tf-incus
operator k8s, k8s-operator, node-effects, ssh, tf, tf-incus, tf-incus-operator

The methods give three variants of the topology:

  • Machines that exist. The methods ssh and tf deploy to machines that are there before the deployment. The methods provision nothing (core/setups/ssh-hosts.nix, core/setups/tf-hosts.nix).
  • Incus instances. The methods tf-incus and tf-incus-operator make one Incus instance for each node on the hypervisor, and then deploy to it (core/setups/tf-incus-hosts.nix, core/setups/tf-incus-operator-hosts.nix).
  • Kubernetes pods. The methods k8s and k8s-operator run each node as a pod in a cluster (core/effects/k8s/effect.nix).

The method node-effects deploys nothing.

Incus instances

flowchart TB
  internet(["Internet"])
  operatorPerson(["Operator"])
  admins(["Contributors and administrators"])
  upstream(["Upstream sources and binary caches"])
  backupTarget(["External backup target"])

  subgraph dev["Group dev: machines that exist"]
    dev_dev2["dev2<br/>component forgejo-runner"]
    dev_dev3["dev3<br/>component forgejo-runner"]
    dev_dev4["dev4<br/>component forgejo-runner"]
    dev_fediversity_ci["fediversity-ci<br/>component forgejo-runner"]
    dev_forgejo["forgejo"]
  end

  subgraph hypervisor["Group hypervisor: machine incus"]
    daemon["Incus daemon<br/>API on port 8443"]
    gateway["Bridge address 10.0.100.1<br/>dnsmasq and nix-serve"]
    public["Public segment<br/>bridge pubbr0, second interface eth1 of a node"]
    subgraph bridge["Bridge incusbr0: 10.0.96.0/20"]
      subgraph provider["Group hosting-provider: 10.0.100.2-10.0.100.254"]
        hosting_provider_authelia["authelia"]
        hosting_provider_bind["bind"]
        hosting_provider_garage["garage"]
        hosting_provider_lldap["lldap"]
        hosting_provider_netbox["netbox"]
        hosting_provider_openbao["openbao"]
        hosting_provider_otel_collector["otel-collector"]
        hosting_provider_overview["overview"]
        hosting_provider_postgresql["postgresql"]
        hosting_provider_smtp["smtp"]
        hosting_provider_spire["spire"]
        hosting_provider_telemetry["telemetry"]
        hosting_provider_terraform_backend["terraform-backend"]
        hosting_provider_valkey["valkey"]
        hosting_provider_windmill["windmill"]
        leaves["api and panel<br/>nodes that the leaves add"]
      end
      subgraph operator["Group operator: one /28 of 10.0.104.0/21 for each deployment"]
        subgraph operatorPublished["Roster published"]
          operator_mastodon["mastodon"]
          operator_peertube["peertube"]
          operator_pixelfed["pixelfed"]
        end
        subgraph operatorInternal["Roster internal"]
          operator_authelia["authelia"]
          operator_grafana["grafana"]
          operator_lldap["lldap"]
          operator_smtp["smtp"]
          operator_valkey["valkey"]
        end
      end
    end
  end

  internet -->|"boundary 1"| public
  public --> provider
  public --> operatorPublished
  internet -->|"boundary 2: ports 22 and 8443"| daemon
  operatorPerson -->|"boundary 3"| leaves
  operator -->|"boundary 4: ports of the first table below"| provider
  operator -->|"ports of the second table below"| gateway
  provider --> gateway
  dev -->|"boundary 6"| daemon
  admins -->|"boundary 7"| hypervisor
  upstream -->|"boundary 8"| gateway
  daemon -->|"makes the instances"| bridge
  operator -->|"boundary 5"| hosting_provider_openbao
  hosting_provider_garage -->|"boundary 9"| backupTarget

The diagram shows the hosting-provider group, one deployment of the operator group, the hypervisor and the dev group. The numbers are the numbers of the trust boundaries in the threat model.

Instances

  • One Incus daemon on the hypervisor runs the instances of the hosting provider and of all operator deployments (core/setups/groups/operator.nix).
  • The name of an instance of an operator deployment is <namespace>-<node>. The deployment of the hosting provider has an empty namespace (core/setups/tf-incus-hosts.nix).
  • An instance is a container by default. The setting instanceType accepts container and virtual-machine (core/effects/common/effect-meta.nix).
  • A node that uses the /nix/store of the hypervisor must be a container. A deployment can use virtual-machine only if none of its nodes uses that store (core/effects/tf/incus/instance.tf).
  • A container gets a software TPM 2.0 by default (enableTpm in core/effects/common/effect-meta.nix). The SPIRE agent of the node uses it to attest.
  • The deployment program does not use SSH into a container. It connects to the Incus API on port 8443 of the hypervisor and uses incus exec (core/setups/tf-incus-hosts.nix).
  • The nodes of the hosting-provider group that start deployments have a client credential for the Incus API (deployRunnerApps in core/setups/hosts-common.nix).

Network segments

All instances are on one bridge of the hypervisor, with IPv4 only. The bridge is the interface eth0 of each instance.

Segment Value Use
Bridge subnet 10.0.96.0/20 All instances on the hypervisor
Bridge address 10.0.100.1 The hypervisor: gateway, resolver and substituter of the instances
Provider prefix 10.0.96.0/21 The nodes of the hosting provider
DHCP range 10.0.100.2-10.0.100.254 The leases of the nodes of the hosting provider
Operator prefix 10.0.104.0/21 One /28 for each operator deployment, with static addresses
  • The dnsmasq service of the bridge gives each node of the hosting provider a lease and a name (core/components/hosting-provider/incus-bridge.nix).
  • Each operator deployment gets a /28 from NetBox. An Incus network ACL on each interface of such a node drops the traffic to and from the other operator deployments (core/components/hosting-provider/incus-bridge.nix).
  • A node of the hosting provider accepts internal traffic from the bridge subnet. A node of an operator deployment accepts internal traffic from the provider prefix and from the addresses of its own deployment (internalSources in core/setups/hosts-common.nix).
  • The public segment is a second interface, eth1, of an instance. With the uplink mode nat, which is the default of the Incus methods, an instance has no such interface (core/setups/wiring/public-uplink.nix). On the hypervisor, the bridge pubbr0 on a VLAN interface is the parent of the public interfaces (core/setups/hosts/incus.nix).
  • A port of a node is open to the internal sources only, until the component that serves the port publishes it (core/setups/wiring/node-selection.nix, core/resources/firewall).

A node of an operator deployment can connect to these ports of the nodes of the hosting provider, by the resource that the node of the hosting provider supplies (core/components/hosting-provider/operator-reach.nix):

Resource TCP ports UDP ports
objectStore 443, 3900 --
oidc 443 --
otelBackend 443 --
pki 443, 8083 --
postgresql 5432 --
smtp 587 --
spiffe 8081 --
varsBackend 8200 --

The nodes on the bridge can connect to these ports of the hypervisor (core/components/hosting-provider/hypervisor-reach.nix, core/components/hosting-provider/operator-reach.nix):

Source TCP ports UDP ports
A node of the hosting provider 22, 53, 443, 5000, 8443 53, 67
A node of an operator deployment 53, 5000 53, 67

Kubernetes pods

flowchart TB
  internet(["Internet"])
  registry["Image registry"]

  subgraph cluster["Kubernetes cluster"]
    balancer["Load balancer or shared Gateway"]
    subgraph namespace["Namespace of one operator deployment: default-deny NetworkPolicy"]
    subgraph pod_authelia["Node authelia"]
      pod_authelia_deployment["Deployment authelia<br/>one pod"]
      pod_authelia_state[("PersistentVolumeClaim<br/>authelia-state")]
      pod_authelia_deployment --- pod_authelia_state
    end
    subgraph pod_grafana["Node grafana"]
      pod_grafana_deployment["Deployment grafana<br/>one pod"]
      pod_grafana_state[("PersistentVolumeClaim<br/>grafana-state")]
      pod_grafana_deployment --- pod_grafana_state
    end
    subgraph pod_lldap["Node lldap"]
      pod_lldap_deployment["Deployment lldap<br/>one pod"]
      pod_lldap_state[("PersistentVolumeClaim<br/>lldap-state")]
      pod_lldap_deployment --- pod_lldap_state
    end
    subgraph pod_smtp["Node smtp"]
      pod_smtp_deployment["Deployment smtp<br/>one pod"]
      pod_smtp_state[("PersistentVolumeClaim<br/>smtp-state")]
      pod_smtp_deployment --- pod_smtp_state
    end
    subgraph pod_valkey["Node valkey"]
      pod_valkey_deployment["Deployment valkey<br/>one pod"]
      pod_valkey_state[("PersistentVolumeClaim<br/>valkey-state")]
      pod_valkey_deployment --- pod_valkey_state
    end
    subgraph pod_mastodon["Node mastodon"]
      pod_mastodon_deployment["Deployment mastodon<br/>one pod"]
      pod_mastodon_state[("PersistentVolumeClaim<br/>mastodon-state")]
      pod_mastodon_deployment --- pod_mastodon_state
    end
    subgraph pod_peertube["Node peertube"]
      pod_peertube_deployment["Deployment peertube<br/>one pod"]
      pod_peertube_state[("PersistentVolumeClaim<br/>peertube-state")]
      pod_peertube_deployment --- pod_peertube_state
    end
    subgraph pod_pixelfed["Node pixelfed"]
      pod_pixelfed_deployment["Deployment pixelfed<br/>one pod"]
      pod_pixelfed_state[("PersistentVolumeClaim<br/>pixelfed-state")]
      pod_pixelfed_deployment --- pod_pixelfed_state
    end
    end
  end

  subgraph provider["Nodes of the hosting provider, not in the cluster"]
    openbao["openbao"]
    spire["spire"]
  end

  internet -->|"boundary 1"| balancer
  balancer -->|"Service of a node"| namespace
  registry -->|"image of a node"| namespace
  namespace -->|"boundary 5"| openbao
  namespace -->|"attestation of the SPIRE agents"| spire

The diagram shows one deployment of the operator group with the method k8s-operator. core/effects/k8s/effect.nix makes these objects:

  • Each node is one Deployment with one pod. The pod runs an image of the NixOS configuration of the node (core/setups/nixos/k8s-pod.nix).
  • Each node has the PersistentVolumeClaim <node>-state on /var/lib. The other paths of the pod start again from the image.
  • Each role of a node is one more Deployment, <node>-<role>. A role with a service runs the image of that service as an unprivileged user.
  • A NetworkPolicy lets no connection in to the pods of the namespace. One more NetworkPolicy for each node lets in what the firewall resource of the node permits.
  • A node with ports for sources outside of the cluster has a Service of the type LoadBalancer.
  • The method k8s-operator appends the namespace of the deployment to the name of the Kubernetes namespace. It publishes the names of a node at the address of its Service. It can also connect the nodes to a shared Gateway with a TLSRoute and an HTTPRoute.
  • The OpenBao and SPIRE servers stay outside of the cluster. A pod has no TPM, thus the SPIRE agent of a pod attests with the token of its service account.

The deployment does not make the cluster, the image registry, the RuntimeClass, the storage class and the load balancer. They must be there before the deployment.

Trust boundaries in the topology

Boundary Where it is in the topology
1. Internet to the public services The public segment, or the load balancer of a cluster, to the published ports of a node.
2. Internet to the administrative surface SSH on port 22 of a machine, and the Incus API on port 8443 of the hypervisor.
3. Operator to the hosting provider The nodes panel and api of the hosting-provider group.
4. Operator deployment to the hosting-provider mesh The bridge between an operator deployment and the nodes of the hosting provider.
5. Node to the secret store Each node to the node openbao of the hosting-provider group.
6. Forge to CI and CD The machines of the dev group to the machines that they deploy.
7. Contributors and administrators to the nodes SSH to each machine and to each node.
8. Upstream and caches to the Nix store The substituters of the hypervisor, and the nix-serve service of the hypervisor for the instances.
9. Garage node to the external backup target The node garage of the hosting-provider group to an S3 service of a different provider.

The five functions

This part tells how the system does each of the five functions of the scope.

Install applications

The operator selects applications in the form of panel. core makes the form data: a JSON Schema of the options of each application, and the catalog of the applications. api gives them to the browser (/schema, /applications). When the operator deploys, the browser sends the configuration to api (/deployments/submit/{id}). api asks panel if this account can change this deployment, validates the configuration, puts a build in the queue, and then runs the deployment program of core. The deployment program makes the machines on the Incus hypervisor, the DNS records, the certificates and the secrets, and it installs the NixOS configuration of each machine.

Details: Make a deployment in the user guide of the panel, the design of the API, and Add an application for the application side.

Configure applications

The options of an application are the options of its component in the data model of core (Operator configuration). panel shows them as a form. A change of the configuration is a new submit: api validates the new configuration and deploys it. core also gives a table of option renames, and api uses the table to move a stored configuration to the current options (/schema/migrations).

Details: the schema and migration sources of the API, and the data model.

Update applications

The version of an application comes from the revision of the code that a deployment uses, and that revision pins nixpkgs. Thus an update is a deploy with a newer revision. Each deployment has an update policy in api (/deployments/update_policy/{id}): manual, automatic or window. With manual, the operator starts the deploy. With the other two policies, api starts the deploy when it finds a new default source, immediately or in a maintenance window.

Details: the update policy of the API.

Manage users

There are two sets of users:

  • The accounts of the operators are in panel. An operator logs in with the single sign-on of the hosting provider or with a local account. The hosting provider manages the accounts in the administration pages of panel. panel keeps which account owns which deployment, and api asks panel before it does an action.
  • The users of the applications are in the deployment of the operator. Each deployment has its own lldap directory and its own Authelia single sign-on. core connects each application to them through the ldap and oidc resources. The operator adds users in lldap, and the groups in lldap give the roles in the applications.

Details: Manage the users of your deployment for the operator, Manage users and groups for the hosting provider, and the architecture page of the panel for the accounts.

Switch an operator to and from a different hosting provider

A move has a source and a target, and each is an installation of Fediversity.

  • At the source, api exports the deployment (/migration-string/{id}). The core/effects/migration effect stops the services of each application and makes a snapshot of its data. The result is a migration string. It holds a pointer to the snapshot of each application, and the transfer code of the domain when the registrar account of the source holds the domain.
  • At the target, api imports the string (/migration-import/{id}) and deploys the configuration. The core/effects/migration-restore effect restores each application from the snapshot of the source. The target can transfer the domain to its own registrar account.

The configuration of an operator can name its own module source. Thus the operator can move away from a hosting provider that uses a different revision of the code.

Details: Export a deployment for a migration and Restore the apps of a migration in the hosting-provider guide, the export of a migration in the design of the API, and the switching and exit clauses of the contract templates.

Metering and accounting

This part tells what the system measures about a deployment of an operator, where each number comes from, and where a hosting provider reads it. It also tells what the system does not measure.

The system gives the quantities. It does not calculate a price and it does not make an invoice. The only amounts of money in the system are the costs that the registrar gives for a domain.

The sources

The system has three sources, and each one has its own store.

Source What it gives Store Where to read it
The builds of api/ The periods in which a deployment ran one configuration The build table of the state store of api/ GET /usage, field periods
The domain events of api/ What the registrar account of the hosting provider charged for the domain of a deployment The domain_event table of the same state store GET /usage, field domain_events
The telemetry of the hosting provider The storage, the compute and the network traffic of a deployment, and figures of its applications Mimir, on the telemetry node Grafana, or a query on Mimir
flowchart LR
  subgraph api["api/"]
    builds["build table"]
    events["domain_event table"]
    usage["GET /usage"]
    builds --> usage
    events --> usage
  end
  deploy["Deploy job"] -- "result and domain.json" --> builds
  deploy --> events
  snapshot["Snapshot of the registrar account"] -- "renew, release" --> events

  subgraph telemetry["Telemetry of the hosting provider"]
    collector["otel-collector"]
    mimir["Mimir"]
    grafana["Grafana"]
    collector --> mimir --> grafana
  end
  storage["PostgreSQL and garage nodes"] -- "size of each database and bucket" --> collector
  hypervisor["Incus hypervisor"] -- "use of each instance" --> collector
  apps["Application nodes"] -- "application metrics" --> collector

  provider["Hosting provider"]
  usage --> provider
  grafana --> provider

The two sources of api/ and the telemetry have no link in the code. api/ does not read Mimir, and the telemetry does not read the state store of api/. The deployment id is the common key: it is deployment_id in the result of GET /usage and it is the label deployment_id of the metrics.

Usage periods

GET /usage (usage_list, api/src/fediversity_api/routes/usage.py) gives the usage periods. usage_list of api/src/fediversity_api/procedures.py calculates them at each call from the builds that succeeded (list_succeeded_builds of api/src/fediversity_api/state.py). api/ does not store a period.

  • Each build that succeeded and is not a destroy starts a period when it finishes. The next build of the deployment that succeeds ends the period. A destroy also ends it. The last period of a deployment that is not destroyed has end set to null.
  • A build that failed or was canceled changes no period.
  • A period has the deployment_id, the build_id, the start, the end, the configuration and the module_source of its build, and the flag suspended. A suspended deployment stops its instances and keeps its volumes.
  • A period has the domain that its configuration serves the applications below, and the registered_domain that the deployment holds at the registrar account of the hosting provider in that period, if any.
  • api/ keeps the builds of a deployment after its destroy. Thus the periods of a destroyed deployment stay available.

The params since and until select the periods that overlap that time. api/ does not cut a period to these times. The param id selects deployments, and a caller can give it more than one time.

A period follows the builds and not the instances. It tells which configuration a deployment ran and when. It does not tell how much storage or compute the deployment used. The telemetry gives these quantities.

Domain events

The field domain_events of the same result tells what the registrar account charged for the domains of the deployments. api/src/fediversity_api/domain_events.py records the events in the domain_event table.

Kind What records it Cost
register, transfer The end of a deploy that registered the domain or transferred it into the account The cost that the registrar gave for the order
adopt The end of a deploy that took a domain that the account held already None
renew The unit fediversity-api-domain-renewals, when the expiry in the snapshot of the account moved forward The renewal price of one year, for each year
release The same unit, when the domain left the account None

An event has its domain, its deployment_id, its cost in minor units of its currency, and the expiration_date of the registration after the event. The cost is null when the registrar charged nothing or when api/ does not know the charge.

api/ records renew and release only when registrarSnapshot of the NixOS module of api/ (api/nix/configuration.nix) points to the snapshot of the registrar account. The unit runs when the snapshot changes.

The events stay after the destroy of their deployment. The page api/docs/design/usage.md has the full rules.

Who can read the usage

GET /usage has the authorization of all routes of api/: api/ asks the authorization callback of the front-end, and the callback gives the deployments that the caller can see (api/src/fediversity_api/authz.py). api/ then limits the result to these deployments.

When the panel is the front-end, the rule of usage_list is that the caller owns the deployments (AUTHZ_PROCEDURE_RULES in panel/src/panel/views.py):

  • The callback gives the deployments of one account, also for a superuser. Thus one call gives the usage of one operator.
  • The callback includes the deployments that the operator deleted. The panel keeps the row of a deleted deployment for this purpose, until the removal of the account of the operator (panel/src/panel/models.py).
  • A backend of the hosting provider can call api/ with the service token of api/ and the header X-On-Behalf-Of with the user name of an account. The callback then decides as for that account.

The panel has no page that shows the usage. No view of the panel calls usage_list; the panel only answers the authorization callback for it. A hosting provider reads GET /usage with its own tool.

Resource use in the telemetry

The telemetry gives the quantities that the usage periods do not have. Each metric below has the label deployment_id, and the metrics are in Mimir.

Quantity Metrics Source
Size of each PostgreSQL database pg_database_size_bytes The PostgreSQL exporter on the node that runs PostgreSQL (core/components/hosting-provider/node-metrics.nix), each 30 seconds
Size of each garage bucket garage_bucket_bytes, garage_bucket_objects, garage_bucket_unfinished_multipart_upload_bytes core/components/hosting-provider/garage-usage-exporter.py, which asks the admin API of garage at each scrape, each 60 seconds
Use of each instance The incus_ metrics of the hypervisor, for example incus_cpu_seconds_total and incus_filesystem_size_bytes, which the dashboard core/components/operator/grafana/dashboards/resources.json shows The scrape incus of the otel-collector (core/setups/hosts-common.nix), each 30 seconds
Host metrics of each node The system_ metrics: CPU, memory, load, disk, filesystem, network and paging The unit otel-node-metrics of the node (core/components/hosting-provider/node-metrics.nix), each 30 seconds
Figures of an application The nodeinfo_ gauges, for example nodeinfo_users_total, and the Prometheus endpoints that an application gives The scrape job app-metrics of the otel-collector (core/components/hosting-provider/app-metrics-discovery.nix)

The label deployment_id comes from a name that the deployment cannot change after the fact:

  • For a database, a bucket and an instance, a relabel rule takes it from the name of the database, the bucket or the instance (core/components/hosting-provider/deployment-relabel.nix). A database, a bucket or an instance of the hosting provider has no deployment_id.
  • For the host metrics of a node of an operator deployment, the telemetry node sets it from the path of the request, which opens only with the credential of that deployment (core/components/hosting-provider/deployment-ingest.nix).
  • For the application metrics, it comes from the KV path of the endpoint entry (core/components/hosting-provider/app-metrics-discovery.py). A deployment can write only below its own id. A deployment_id label that an application gives becomes exported_deployment_id.

The hosting provider reads these metrics in Grafana or with a query on Mimir. The dashboard "Deployment storage" (core/components/hosting-provider/dashboards/deployment-storage.json) shows the size of the databases and the buckets of each deployment. No dashboard shows the usage periods or the domain events.

Mimir keeps the metrics for services.fediversity-telemetry.retentionDays (core/components/hosting-provider/telemetry.nix). After that time, the resource use of a past period is not available. The usage periods and the domain events have no retention: api/ does not remove a build or a domain event.

Know the use of the deployments for billing in the hosting provider guide gives the procedure and example queries.

What the system does not do

  • It has no prices, no tariffs and no invoices. A domain event has the cost that the registrar gave, and no other amount is in the system.
  • It does not add the resource use to the usage periods. The hosting provider joins the two on the deployment id.
  • It does not aggregate the metrics for a billing period. The hosting provider does this with a query.

Logs, traces and audit records

This part lists each channel that records what the software and its users do: the audit events, the logs, the traces and the job records. For each channel it gives the personal data, the purpose of the data, the store, the persons who can read it, the retention, the control that keeps credentials out of it, and the test that shows the control.

The page describes what the code does. It does not say what the law requires or permits. Where a legal question applies, the page says "Open". The hosting provider decides it, see the hosting provider guide.

The stores themselves (location, owner, encryption, backup, erasure) are in the state register. The roles and the directory groups are in the access model.

The tables that have the label "generated" come from the code at each build of the documentation (core/docs/md/technical-design/observability-records.nix).

Words on this page

  • "None" means that the code was examined and has no such control, test or limit.
  • "Open" means that no source in the tree gives the fact, or that the question is a legal one.
  • "A person with root on the node" is a person who can open a shell as root on that node. The access model says who that is.

How a record goes from a node to the stores

flowchart TD
  units["Units of a node"]
  journal["Journal of the node"]
  nodeLogs["Node log shipper (otel-node-logs)"]
  auditShipper["Audit shipper (fediversity-audit-shipper)"]
  collector["otel-collector"]
  lokiDefault["Loki, default tenant"]
  lokiAudit["Loki, audit tenant"]
  tempo["Tempo"]
  grafana["Grafana"]
  apiTable["audit_event table of api/"]
  api["api/"]

  units --> journal
  api --> apiTable
  api --> journal
  journal --> nodeLogs
  journal --> auditShipper
  nodeLogs --> lokiDefault
  auditShipper --> lokiAudit
  collector --> lokiDefault
  collector --> tempo
  lokiDefault --> grafana
  lokiAudit --> grafana
  tempo --> grafana

The nginx of the telemetry node sets the tenant of each request (X-Scope-OrgID), thus a client cannot select a tenant (core/components/hosting-provider/telemetry.nix).

Retention

The hosting provider sets the retention periods. Each period below is the default of a setting, and the hosting provider can change it in its configuration. The reason for a period, and the law that applies to it, are not in the code: Open.

Generated:

Records Setting Default Source
The audit_event table of api/ services.fediversity-api.auditRetentionDays 365 days. 0 keeps all events. api/nix/configuration.nix
The tenant audit of Loki services.fediversity-telemetry.auditRetentionDays 365 days. 0 keeps all lines. core/components/hosting-provider/telemetry.nix
The tenant fake of Loki, Tempo and Mimir services.fediversity-telemetry.retentionDays 90 days core/components/hosting-provider/telemetry.nix
The journal of each node MaxRetentionSec and SystemMaxUse of services.journald.settings.Journal 30day, and at most 512M core/setups/nixos/common.nix
The journal lines that a node of the hosting provider sends to Loki services.fediversity-node-logs.priority The lines at level info and higher core/components/hosting-provider/node-logs.nix
The completed jobs of Windmill services.fediversity-windmill.jobRetentionDays 30 days core/components/hosting-provider/windmill/windmill.nix
The build logs and the operation_run table of api/ services.fediversity-api.logRetentionDays 30 days. 0 keeps all logs and runs. api/nix/configuration.nix
The operation_run table of api/ The constant OPERATION_RUNS_KEPT The last 100 runs of each deployment api/src/fediversity_api/state.py
The nginx access log of the forge node services.logrotate.settings.nginx 182 daily files core/components/dev/forgejo/forgejo.nix

Limits that are not a setting of this software:

  • The nginx access log of a node other than the forge node: the logrotate rules of nixpkgs keep 26 weeks of files (the comment in core/setups/nixos/nginx.nix).
  • The rows of the build table of api/: None. No job removes old rows. logRetentionDays removes only the log of a row.
  • The disk queue of the audit shipper: None. The module sets no limit of size or of age.
  • The logs of the CI jobs and the change log of NetBox: Open.

Channels

Audit events of the api

api/ records each action on a deployment as one audit event (api/src/fediversity_api/audit.py).

  • Store: the audit_event table of the database of api/, and one JSON line on the logger fediversity_api.audit. The line goes to the journal, and then to Loki, see Audit lines in Loki.
  • Readers: an operator reads its own events through the panel (recent_actions, GET /audit-events). A superuser of the panel reads the events of each account (reads_all_history). A person with access to the database reads the table.
  • Retention: services.fediversity-api.auditRetentionDays. api/ removes the older events when it starts and each time that it records an event (prune_audit_events in api/src/fediversity_api/state.py). The events of a deleted deployment stay until they are older than the retention.
  • Control that keeps credentials out: an event holds only the fields of the table below. For a request that is not valid, reason gives the location of the error and not the value. For an import, the event holds only the result. api/ cuts a long reason of the authorization callback, and cuts request_id to 128 characters.
  • Test: api/src/fediversity_api/tests/test_audit.py: test_an_invalid_request_records_where_without_the_value, test_an_invalid_configuration_records_where_without_the_value, test_an_import_records_only_its_result, test_an_unknown_action_stores_nothing_of_the_caller, test_a_long_reason_of_the_callback_is_cut. The retention has the tests test_a_new_event_removes_the_events_older_than_the_retention, test_the_retention_is_a_setting and test_zero_keeps_all_events.
  • Protection from changes: each event has a hash of the event and of the event before it. fediversity-api-audit-verify examines the chain (verify_audit_chain). The tests are test_a_changed_event_is_found, test_a_removed_event_is_found and test_a_prune_keeps_the_chain_correct.

The fields of an event, with the purpose of each. Generated:

Field From Personal data Purpose Serves to trace abuse
actor _record Yes. The OpenID Connect sub of the account, or the primary key of an account without single sign-on. To tell who did an action on a deployment. Yes
actor_issuer _record Yes, together with actor. The OpenID Connect issuer of the account. To tell which identity provider knows the actor. Yes
internal _record No. To tell an action of the system from an action of a person. No
action _record Yes, together with actor. To tell what occurred. Yes
deployment_id _record Indirectly. It links the event to an operator. For domain_contact_put and domain_contact_delete, it is the ID of a contact of the operator. To find the deployment that an action changed. Yes
build_id _record Indirectly. It links the event to a build of a deployment. To find the build that an action started. No
request_id _record No. nginx makes a random value for each request. To tell which audit events come from one request. Yes. It connects the events of one request.
result _record Yes, together with actor. To tell if api/ permitted the action. Yes
reason _record Possibly. It is a text from the code or from the authorization callback. To tell why api/ refused an action. Yes
domain _record Possibly. A domain name can contain the name of a person. To tell who claimed a domain. Yes
domain_terms _record No. It is a version of the terms. To show which terms the actor accepted. No
id The store No. To identify the event. No
created_at The store Yes, together with actor. To tell when the action occurred. The retention uses it. Yes
seq The store No. To detect a removed event. No
hash The store No. To detect a changed event. No

An audit event does not hold the client address. To connect an event to a client address, a person must find the journal line or the access log line of the same request, and the access log holds only the network of the client.

api/ records the first refused call without a login in each interval of 300 seconds, and then one event with the number of the other calls. Thus an anonymous caller cannot fill the table.

Audit lines in Loki

The audit shipper of a node (core/components/hosting-provider/audit-shipper.nix) reads the journal of the unit of a component, keeps the lines that match the pattern of the component, and sends them to the audit tenant of Loki. Generated:

Source Unit that writes the lines Pattern of an audit line Tenant of Loki Other tenants
api fediversity-api.service [A-Z]+:fediversity_api\.audit: audit None. The node log shipper drops these lines, thus they do not go to the tenant fake.
  • Personal data and purpose: the same fields as the audit event.
  • Store: the audit tenant of Loki. Before the shipper sends a line, the line is in its disk queue in /var/lib/fediversity-audit-shipper. The shipper does not stop its retries, thus a line stays in the queue while the telemetry node does not accept it.
  • Readers: each account that can log in to the Grafana of the hosting provider, thus the members of fediversity-admins and fediversity-staff. Grafana has one organization and the datasource Loki audit has no permission of its own, thus a Viewer can query the tenant. A person with root on the node of api/ reads the queue.
  • Retention: services.fediversity-telemetry.auditRetentionDays. The tenant refuses the deletion API of Loki, thus only the retention removes a line. The setting of api/ does not apply to these lines.
  • Control that keeps credentials out: the control of the audit event. The shipper adds none.
  • Test: None for the content of a shipped line.

The deploy credentials can change or delete the tenant, see the hosting provider guide.

Journal of each node

  • Personal data: what each unit writes. sshd, Authelia and the nginx error log write full client IP addresses and user names. maddy writes email addresses. The applications write what their log level gives.
  • Purpose: to find the cause of a failure on a node (the comment in core/setups/nixos/common.nix). A purpose for each field: Open.
  • Store: the journal of the node. A node of the hosting provider sends the lines at the level of services.fediversity-node-logs.priority and higher to the default tenant of Loki (core/components/hosting-provider/node-logs.nix). A node of an operator deployment sends the lines at level warning and higher, and the telemetry node sets the label deployment_id (core/components/hosting-provider/deployment-ingest.nix).
  • Readers: a person with root on the node reads the journal. Each account that can log in to the Grafana of the hosting provider reads the copy in Loki, for all deployments.
  • Retention: see Retention. The lines of a deleted deployment stay in Loki until the retention removes them.
  • Control that keeps credentials out: each unit can write a credential, because a request line or an error message of an application can hold one. Thus the control is in the pipeline and not in each unit. The node log shipper replaces the text that has the shape of a credential with [masked] before a line goes to Loki (core/components/hosting-provider/node-logs-masks.nix). The shapes are: the query string of a request line or of a URL, the user and the password of a URL, the value of an Authorization header, a bearer token, the value of a key whose name contains password, passwd, secret, token, api_key or credential, a JSON Web Token, and a token of OpenBao. A credential that has none of these shapes goes to Loki. The shipper does not read a message that is not UTF-8. The journal of the node keeps the full line: None for the journal.
  • Test: the check node-logs-masks (core/components/hosting-provider/node-logs-masks-test.nix) sends an example of each shape through the masks. The check test-monitoring-storage (core/components/hosting-provider/monitoring-test.py, "the node log shipper masks a credential") writes values to the journal of a node and reads Loki. core/setups/hypervisor-config-test.nix examines the retention settings of the journal.

Audit device of OpenBao

OpenBao writes each request and each response as one JSON line to its standard output (core/components/hosting-provider/openbao/openbao.nix).

  • Personal data: the client IP address, the path and the operation of the request, and the display name and the entity of the token, for example the name that an OIDC login gives to an administrator.
  • Purpose: Open. No source in the tree gives the purpose of a field.
  • Store: the journal of openbao.service, and then the default tenant of Loki. These lines do not go to the audit tenant.
  • Readers: the readers of the journal and of the default tenant, see Journal of each node.
  • Retention: that of the journal and of the default tenant. The lines do not have the retention of the audit tenant.
  • Control that keeps credentials out: OpenBao writes secret values and tokens as HMAC-SHA256 hashes, and elide_list_responses removes the keys of a list response.
  • Test: the check integrations-openbao-pki (core/checks/integrations/openbao-pki/nixosTest.nix, "The audit device writes each request to the journal with hashed values").

nginx access log

Each node that runs nginx writes one line for each request to /var/log/nginx/access.log (core/setups/nixos/nginx.nix).

  • Personal data: the network of the client (the address with its last part removed: /24 for IPv4, /48 for IPv6), the method, the path and the protocol of the request, the referrer without its query string, the user agent, and the user name of HTTP basic authentication. On the vhosts with auth_basic (telemetry ingest, metrics query, audit path) this user name identifies the credential of one deployment, not a person. On other vhosts it is the text that the client sends.
  • Purpose: Open. No source in the tree gives the purpose of a field.
  • Store: files on the node. The forge node does not record the calls of the CI runners (core/components/dev/forgejo/forgejo.nix).
  • Readers: a person with root on the node. The node log shipper reads the journal and not this file, thus the lines do not go to Loki.
  • Retention: see Retention.
  • Control that keeps credentials out: the log format holds no query string, of the request or of the referrer, and no password of HTTP basic authentication. A credential in a path or in the user agent goes to the file.
  • Test: the check nginx-access-log (core/setups/nixos/nginx-access-log-test.nix) sends requests with a query string, a referrer and a user name with a password, and examines the lines. It examines the mask of an IPv4 address. For the mask of an IPv6 address: None.

Request logs of the api and the panel

api/ and the panel run on uvicorn with its access log on (api/src/fediversity_api/http_main.py, panel/nix/configuration.nix). Each request gives one line with the address of the peer, the method, the path with its query string, and the status.

  • Personal data: the query string can hold an identity, for example the actor in GET /audit-events and the account in the history page of the panel.
  • Purpose: Open.
  • Store: the journal of the unit, and then the default tenant of Loki, because the lines are at level info.
  • Readers and retention: those of the journal, see Journal of each node.
  • Control that keeps credentials out: in Loki, the masks of the node log shipper, which replace the query string of a request line, see Journal of each node. In the journal of the node: None.
  • Test: the check node-logs-masks has a request line of uvicorn as an example.

Jobs of Windmill

Windmill keeps the arguments, the result and the log of each completed job.

  • Personal data: a deploy job holds the configuration of the deployment, which can hold email addresses and user names, and the deploy log.
  • Purpose: the job runs the deploy, and api/ reads the log of the job to show it to the operator. A purpose for a completed job: Open.
  • Store: the database of Windmill, see the state register.
  • Readers: the administrators of Windmill. An operator reads the log of the builds of its own deployments through api/ (build_logs in api/src/fediversity_api/procedures.py).
  • Retention: services.fediversity-windmill.jobRetentionDays.
  • Control that keeps credentials out, deploy job: api/ removes the secret keys from the deploy_env that it sends to the job. Nothing removes a value from the text of the deploy log: None.
  • Control that keeps credentials out, node-effect job (f/fediversity/node_effect): the arguments and the result of this job can hold bearer credentials, and Windmill keeps them in clear. api/ deletes the job when it is completed, with a success or a failure, before the result goes back to the caller. When api/ cannot delete a job, it writes the ID of the job to its log, and a person must delete the job (_run_effect_job in api/src/fediversity_api/node_effect.py). Until then, the credentials stay in the job.
  • Test: api/src/fediversity_api/tests/test_deploy_env.py (SecretKeysStayOffTheWindmillPayload) for the deploy job. api/src/fediversity_api/tests/test_migration.py for the node-effect job: test_windmill_route_runs_the_job_and_deletes_it, test_a_failed_job_reports_its_error_and_is_deleted, test_a_timed_out_job_fails_and_is_deleted, test_a_job_that_cannot_be_deleted_is_logged_by_id.

Build logs and operation output in the api

  • Personal data: the actor of each run of an operation (operation_run table). The text of a build log and of an operation output can hold what the deploy tools and the operation write.
  • Purpose: api/ shows the result of a build or of an operation to the operator.
  • Store: the build and operation_run tables of the database of api/. With Windmill, api/ reads the build log from the job. Without Windmill, api/ keeps the last 64 KiB of the output of a failed build in the build table.
  • Readers: the operator of the deployment, through api/. A person with access to the database.
  • Retention: api/ keeps the last runs of each deployment, see Retention, and deletes the runs after the deployment is destroyed. With services.fediversity-api.logRetentionDays, api/ also removes the log of a build and each run of an operation that finished before that number of days (prune_logs in api/src/fediversity_api/state.py). It does this when it starts, and each time that it stores a build log or a run. The default is 30 days. 0 keeps all logs and runs. The rows of the build table have no limit, because the usage records come from them: None.
  • Control that keeps credentials out: api/ does not keep the output of an operation that has sensitive set. For the text of a build log, see the deploy job in Jobs of Windmill: the same runner writes the two logs.
  • Test: api/src/fediversity_api/tests/test_operations.py: test_the_output_of_a_sensitive_operation_is_not_kept, test_the_deployed_revision_decides_if_the_output_stays. api/src/fediversity_api/tests/test_log_retention.py for the retention. For the build log: api/src/fediversity_api/tests/test_log_credentials.py (FailedBuildLogHoldsNoSecret).

Traces

  • Sources: Grafana sends its own spans when tracingEndpoint is set. NetBox sends the spans of its requests, its HTTP calls and its database queries when fediversity.otel.endpoints.netbox is set (core/components/hosting-provider/netbox/otel.nix). Windmill has the settings but its open-source build sends no spans (core/components/hosting-provider/windmill-otel.nix).
  • Personal data: a span can hold a request path. The attributes that the instrumentation libraries set: Open.
  • Purpose: Open.
  • Store: Tempo.
  • Readers: each account that can log in to the Grafana of the hosting provider.
  • Retention: services.fediversity-telemetry.retentionDays.
  • Control that keeps credentials out: None. The collector has only the batch processor (core/components/hosting-provider/otel-collector.nix).
  • Test: None.

Debug exporter of the collector

When services.fediversity-otel-collector.backends is not set, the collector has only the debug exporter with verbosity = "detailed". It then writes each record that it receives (logs, metrics and spans) to its own journal. The readers, the retention and the missing controls are those of the journal.

Records of Grafana and NetBox

  • Grafana keeps its accounts and its sessions in its own database, see the state register.
  • NetBox keeps a change log with the user of each change. No file in the tree sets its retention or its readers: Open.

Logs of the CI jobs

The forge keeps the log of each CI job. The workflows in .forgejo/workflows/ give secrets to the CD jobs as environment variables.

  • Personal data: Open.
  • Store, readers and retention: Open. No file in the tree sets them.
  • Control that keeps credentials out: Open. No file in the tree sets a mask.
  • Test: None.

Fields that serve to trace abuse

To trace abuse, a person must know who did a thing, from where and when. These fields give that:

  • The audit events of api/: the fields with "Yes" in the last column of the table of the fields. They give the account, the action, the deployment and the time. They do not give a client address.
  • The journal: the full client IP address and the user name in the lines of sshd and Authelia, with the time.
  • The audit device of OpenBao: the client IP address and the display name of the token, with the path and the time.
  • The nginx access log: only the network of the client. It cannot identify one address.

If these fields are sufficient for a duty of the hosting provider to trace abuse, and how long the hosting provider must or can keep them: Open.

Open items

Channel Item State
All The legal basis and the reason of each retention period Open
Audit lines in Loki A test of the content of a shipped line None
Audit lines in Loki A limit on the disk queue of the shipper None
Audit lines in Loki A limit on which Grafana account reads the tenant None
Journal A control for a credential that has no known shape, and for the journal of the node None
Journal A purpose for each field Open
Audit device of OpenBao The retention of the audit tenant None
Audit device of OpenBao A purpose for each field Open
Jobs of Windmill A purpose for a completed job Open
nginx access log A purpose for each field Open
nginx access log A test of the mask of an IPv6 client address None
Request logs of api/, panel A purpose Open
Request logs of api/, panel A control that keeps credentials out of the journal of the node None
Jobs of Windmill A control on an encoded or a derived credential in the text of a deploy log or a build log None
Jobs of Windmill An alert when the sweep cannot delete a node-effect job None
Build logs in api/ A retention of the rows of the build table None
Traces The attributes of a span, and the purpose Open
Traces A control that keeps credentials out, and a test None
debug exporter A control that keeps credentials out None
Change log of NetBox The retention and the readers Open
Logs of the CI jobs The personal data, store, readers, retention and mask Open

Requirements of hosting providers and design review

This page lists the requirements that the sources of the project record for hosting providers. For each requirement, it gives the source and the place in the technical design that answers it. It also gives the record of the review of the design.

The specifications of core, of api and of panel hold the requirements that the project wrote for the code. They are not statements of hosting providers, thus this page does not repeat them. The part traceability connects them to the design.

Sources

The repository of the code holds no statement that a hosting provider made. The sources below are outside of this repository.

Source What it is Who made the statements Public
Front-end interview The summary of an interview of 2026-10-12 that collected requirements for the front end of the panel. The file is meeting-notes/2026-10-12-meeting-summary-front-end-req.md in the meta repository of the project. One person of the hosting provider that is a partner of the project Yes. This page gives a summary only.
Architecture notes The notes architecture-docs/architecture.md and architecture-docs/data-model-requirements.md in the meta repository of the project. These notes are older than the present code. The project Yes. This page gives a summary only.
White paper The white paper of the project, "Server apps commodified: digital autonomy for the masses", in the white-paper repository of the project. The project Yes. This page gives the goals in summary.

The front-end interview is the only source that records the words of a hosting provider. It has these limits:

  • It has the statements of one person of one hosting provider.
  • Its subject is the front end of the panel. It does not cover the infrastructure, the metering or the move between hosting providers.
  • The person is also a member of the project. The record does not tell which statements the person made as a hosting provider.
  • Some statements are ideas for a later version. The tables below mark them.

No source records requirements from other hosting providers.

Requirements from the front-end interview

The column "Answer" tells if the design answers the requirement: "Yes", "In part" or "No". "No" means that the code and the pages of the design show no function for the requirement.

Users and access

ID Requirement Answer Place in the design
I-1 The panel is primarily for operators, who are the customers of a hosting provider. Yes Scope and the actors.
I-2 The hosting provider is responsible for the authentication. A login with a user name and a password is sufficient. Yes Manage users: an operator logs in with the single sign-on of the hosting provider or with a local account. The part access-model has the details.
I-3 The operator makes user accounts and gives the users access to the applications. Yes Manage users: each deployment has its own lldap directory and its own Authelia single sign-on.
I-4 An operator makes the first user accounts in less than 30 minutes. No The design gives the steps (Manage the users of your deployment). No test measures the time.
I-5 A role with which one operator manages other operators. Yes The operators' operator account can manage users' access.
I-6 The hosting provider can see audit logs and the actions of its customers. The interview put this out of its scope. In part api records audit events, and the hosting provider selects the store. See Choose the store and the retention of the audit logs and Know which personal data the logs keep. The panel shows to an operator the history of a deployment and of the account (Deploy).

Use of the panel

ID Requirement Answer Place in the design
I-7 The first version operates in a browser on a workstation, preferably Firefox. Yes panel is a web application (Containers). The tests of the panel use Firefox (panel/nix/tests.nix).
I-8 The panel obeys the accessibility standards of the W3C. In part An automated test with axe-core examines the rules of WCAG 2.1 level A and AA (Accessibility). No person did a manual audit (Publish the accessibility statement).
I-9 The first release has English, Dutch, French and German. Yes LANGUAGES in panel/src/panel/settings.py has these four languages. See Translations.
I-10 The panel can show all languages, with text from left to right and from right to left. No The panel has no language with text from right to left.
I-11 Later: access from a mobile application. No The design has no mobile application.
I-12 The operator changes all settings in the web interface, and then one action deploys the full environment. Yes Install applications: the browser sends the full configuration of the deployment to api in one submit, and api puts one build in the queue.
I-13 Idea: before the deploy, the panel shows a list of the changes, and possibly the cost of the deploy. No The panel shows no cost of a deploy. The pages of the design describe no list of the changes before a deploy.
I-14 An onboarding sequence: upload a company logo, select the company colors, select the applications. In part The operator selects the applications in the form (Install applications). The panel has no onboarding sequence and no setting for a logo or for colors.
I-15 Slow tasks, such as the registration of a domain, start first and continue while the operator makes the other choices. No The deploy starts when the operator clicks "Deploy" (Deploy).

Applications and releases

ID Requirement Answer Place in the design
I-16 The operator sees all available applications and can turn each application on or off. Yes Install applications: core makes the catalog of the applications, and the form of panel shows it.
I-17 The operator can set limits of use and colors for each application. In part The options of an application are in the form (Configure applications, Operator configuration). The design has no general setting for limits of use or for colors.
I-18 Only the project adds an application to the catalog. A hosting provider can turn an application on or off. In part The catalog comes from the components of the data model of core (Add an application). The design has no setting with which a hosting provider removes an application from the catalog.
I-19 The panel manages DNS, mail and three to four applications. Yes The components of a deployment of an operator list the applications. For DNS, see Assign a domain to a deployment. For mail, see the smtp resource in the system context.
I-20 The project makes one main release each month, on the first day of the month. Fixes can come between releases. In part The project makes a release each month, on day 8, and can make one more release in the same month for a fix (SECURITY.md, Make a release).
I-21 The operator sees which changes a release has. In part The notes of a release give the titles of its pull requests (Make a release). The panel does not show them.

Requirements that the project wrote for hosting providers and operators

These statements are from the project. No source records that a hosting provider gave them.

ID Requirement Source Place in the design
P-1 An operator selects applications, and the system deploys them to the domain of the operator. Architecture notes, white paper Install applications.
P-2 An operator can change the configuration of an application in a web interface. The defaults are sufficient for most operators. Architecture notes, white paper Configure applications.
P-3 A hosting provider can give the operator a selection of update policies. Architecture notes Update applications: each deployment has the update policy manual, automatic or window.
P-4 When the options of an application change in an update, the configuration of the operator moves to the new options. White paper Configure applications: the table of option renames.
P-5 An operator can make accounts for other persons and manage their rights. Architecture notes Manage users.
P-6 An operator can move a deployment, with its configuration and its data, to a different hosting provider. Architecture notes, white paper Switch an operator to and from a different hosting provider.
P-7 A move is possible when the two hosting providers do not use the same version of the code. Architecture notes Switch an operator to and from a different hosting provider: the module source of an operator.
P-8 From a hypervisor, the system makes the resources that an application needs: machines, DNS records, storage, databases, mail and secrets. White paper Install applications and the containers.

Design review

No source records a review of this technical design. The repository of the code, the meta repository and the white-paper repository hold no list of reviewers, no date of a review and no result.

A code review of a pull request on the forge is not a review of the design by hosting providers, thus this page does not count it.

Quality attributes

This page gives, for each quality attribute of the full system, the target, what the code does, how a test shows it, and what the code does not do. A target is a value that a document or the code of this repository states. Where the repository states no target, this page says so. This page does not set a target.

The checks that this page names are attributes of the flake of core. To run one, use nix build ./core#checks.x86_64-linux.<name>. The page Flake attributes describes each check.

Summary

Attribute Target in the repository Test of the target
Availability None None
Recovery point 24 hours plus the duration of the backup run, as a result of the schedule The alert rule backup-stale and the restore checks
Recovery time None None; the restore drill measures one part
Downtime of an upgrade None None
Downtime of a move between providers "In the order of a day", with no data loss (Concepts) None for the time; checks for the data
Number of deployments for each provider "At least 1,000 operators" (Concepts) None
Resources of the smallest installation None None

The specifications in core/specs/, api/specs/ and panel/specs/ have success criteria for each feature. No success criterion gives a time, a percentage or a quantity for an attribute of this page.

Availability

Target. The repository states no availability target: no uptime percentage and no maximum time that a service can be down. The contract templates have a row "Availability and backup" that the hosting provider fills in (Contract templates).

What the code does.

  • Each stateful service runs on one node. The section Know the limits of a single node of the hosting provider guide lists the services. For example, garage has replication_factor = 1 (core/resources/garage/server.nix), and the stores of the telemetry node have replication_factor = 1 (core/components/hosting-provider/telemetry.nix).
  • Grafana alert rules tell the hosting provider about a failure (core/components/hosting-provider/telemetry/alert-rules.nix). unit-failed and unit-stuck find a unit that failed or that does not finish its start. node-absent finds a node that sent host metrics before and sends none now. ingest-silent finds that no node sends metrics. disk-filling, disk-filling-trend and disk-full find a disk that gets full. cert-expiring, cert-expiring-critical and renewal-failed find a certificate that is not renewed. The section Monitor the deployments of the guide tells how to use them.
  • A node counts its boots (core/setups/nixos/boot.nix). The boot loader marks a new generation as good only after the check boot-check-gateway gets an answer from the default gateway. A generation that fails the check reboots, and after the configured number of tries the boot loader starts the last good generation.
  • With the deploy method k8s, a pod role has a replicas option (core/resources/pod-roles/default.nix). When the pod of a new deploy does not become ready, the Deployment continues to run the previous image (core/specs/009-deployment-methods/k8s.md, SC-003).

How it is tested. core/components/hosting-provider/telemetry/alert-rules-test.nix and core/components/hosting-provider/monitoring-test.py test the alert rules. The check integrations-boot-counting tests the boot counting. The check deployment-k8s-hosts runs the role lldap with two replicas. No test measures availability.

What it does not do. There is no replica, no standby and no automatic failover for a stateful service. When a node fails, its services are down until the hosting provider repairs the node or restores it from a backup. The alert rules report a failure; they do not repair it.

Recovery point and recovery time

Target. The hosting provider guide gives the recovery point objective (RPO) of each backup as 24 hours plus the duration of the backup run. This value is a result of the default schedule. Each failed run adds 24 hours. The section Know how much data a restore can lose has the table of the backups, their schedule options and their restore points. The repository states no recovery time objective (RTO).

What the code does.

  • Each off-site backup has a timer with OnCalendar = "daily" and Persistent = true as a default (mkOffsiteBackup in core/components/operator/app-resources.nix). The hosting provider can change the schedule with the options that the table of the guide names.
  • backupRetention in core/components/operator/app-resources.nix gives how long the backups keep their data: 30 days for the backups of an application, and 7 daily, 4 weekly and 6 monthly snapshots for the backups that restore the infrastructure.
  • The media backup copies the buckets of an application (core/resources/garage/media-backup.nix). The external copy copies the backup data to a second location (core/resources/garage/external-copy.nix). garage makes snapshots of its metadata (core/resources/garage/metadata-snapshots.nix).
  • The alert rule backup-failed fires when a backup run fails. The rule backup-stale fires when the last good run of a backup is more than 36 hours old.
  • The restore drill (core/components/shared/restore-drill.nix, option fediversity.restoreDrill.enable) restores the newest database backup into a scratch database each day. It records the duration in the metric fediversity_restore_drill_duration_seconds and the time of the last good run in fediversity_restore_drill_last_success_timestamp_seconds. The alert rules restore-drill-failed and restore-drill-stale use these.

How it is tested.

Check What it restores
integrations-app-database-restore The database of an application
integrations-media-backup The media of an application
integrations-backup-external-copy The backups from the external copy
integrations-bootstrap-backup OpenBao and the states of the terraform backend, on the same node and on a new node
integrations-garage-snapshots The metadata of garage

The nix-unit tests in core/setups/backup-retention-test.nix test the retention. No test measures the age of the newest backup at the time of a failure; the rule backup-stale monitors it on a live installation.

What it does not do. No backup is continuous, thus a restore loses the changes after the newest good run. The duration that the restore drill records is the restore time of one database only. It does not include the time to make a new node, to restore files and media, or to find the failure. No test measures the time of a full restore. After a restore, the hosting provider must do the erasures of personal data again; the part data-subject-requests covers this subject.

Downtime of an upgrade

Target. The repository states no target for the downtime of an upgrade.

What the code does.

  • A deploy builds the new configuration of a node and activates it with switch-to-configuration switch (core/effects/common/deploy.sh). systemd restarts the units that changed. Thus a changed service is down for the time of its restart.
  • A service with state uses systemd.stateMigrations. The unit <name>-migrate-pre.service makes a backup and runs the steps for the version range before the service starts. A failed step restores the backup and keeps the service stopped. A downgrade and a skipped release series are refused with exit status 78. Examples are core/components/hosting-provider/openbao/migrations.nix and core/components/hosting-provider/netbox/migrations.nix.
  • Before an upgrade of an application, mkUpgradeSnapshot in core/components/operator/app-resources.nix makes a backup while the application is stopped. When the backup cannot start, it tries again each 30 seconds. After UPGRADE_BACKUP_TIMEOUT seconds (default 600) it refuses the upgrade with exit status 78. The application then stays stopped with its data at the old version.
  • The API applies updates as the update policy of the deployment tells it: manual, automatic or window (api/src/fediversity_api/update_policy.py, api/src/fediversity_api/automatic_updates.py). With window, an operator selects the period of the day in which an update can start. A window is at least 15 minutes and at most 24 hours.

How it is tested. The check integrations-state-migrations tests the upgrade steps, the refusals and the restore after a failed step. The checks integrations-postgresql-major and integrations-postgres-upgrade-dump test an upgrade of PostgreSQL to a new major version. A check integrations-<service>-series for each service with state, for example integrations-openbao-series and integrations-garage-series, tests an upgrade between release series. api/src/fediversity_api/tests/test_update_policy.py tests the update policy. These tests show that an upgrade keeps the data. No test measures the downtime.

What it does not do. There is no rolling upgrade and no second instance that takes the requests during an upgrade of a node. The downtime of a service is the time of its restart plus the time of its upgrade backup and its migration steps. That time depends on the quantity of data. A refused or failed upgrade keeps the service stopped until a person corrects the cause.

Downtime of a move between providers

Target. Concepts states: "Migrations involve service unavailability in the order of a day (due to DNS propagation) but no data loss." The repository states no more exact value.

What the code does.

  • On the source, migration-quiesce stop stops the services of the applications, and the export makes the data available to the target (core/components/shared/migration-quiesce.nix, core/effects/migration/run.sh). The source deployment is suspended.
  • On the target, the import restores the data and starts the applications (core/effects/migration-restore/).
  • The applications are down from the export until the target has restored them and the DNS records point to the target. The sections Export a deployment for a migration and Restore the apps of a migration of the guide give the procedure. To go back, the hosting provider resumes the source deployment.
  • The DNS time depends on the TTL of the records. The record of a node and the NS records of the apex have a TTL of 300 seconds (nodeTtl and apexNsTtl in core/setups/groups/hosting-provider.nix). The default TTL of a zone from NetBox is 86400 seconds (core/effects/octodns/netbox-zone.py). deSEC does not accept a TTL below 3600 seconds (core/effects/octodns/octodns_fediversity_desec.py).

How it is tested. The checks integrations-migration-restore, integrations-migration-restore-boot-wait and deployment-migration-quiesce test the export, the restore and the stop of the services. The check deployment-registrar-transfer tests the transfer of a domain. api/src/fediversity_api/tests/test_migration.py, test_migration_restore.py and test_migrate.py in the same directory test the API side. These tests show that the data arrives. No test measures the downtime, and no test shows the value "in the order of a day".

What it does not do. A move is not live: the applications do not run on the source and on the target at the same time. The downtime includes the copy of the data, which depends on the quantity of data and on the network between the two hosting providers. The code does not measure it.

Number of deployments for each provider

Target. Concepts states: "We target small- to medium-scale hosting providers with 20+ physical machines, able to scale to at least 1,000 operators." The repository states no other quantity.

What the code does.

  • Each operator deployment gets a /28 prefix from the NetBox prefix for operator deployments (core/effects/tf/incus/operator/bridge-isolation.tf). Thus the size of that parent prefix, which the hosting provider selects, limits the number of deployments.
  • The operator deployments share the PostgreSQL node, the garage node and the other nodes of the hosting provider (Contract templates gives the isolation). The capacity of these single nodes limits the number of deployments.
  • The API builds at most schema_build_concurrency schemas at the same time, with a default of 2 (api/src/fediversity_api/schema_build.py). It runs at most one deploy for each deployment at a time, because the deploy job uses the deployment ID as its concurrency key (api/src/fediversity_api/deployment.py).
  • The alert rules disk-filling, disk-filling-trend and disk-full tell the hosting provider when a disk gets full.

How it is tested. No test. No check makes a large number of deployments, and the repository has no load test.

What it does not do. The code does not spread the load of one stateful service across nodes, and it has no limit that refuses a new deployment when the capacity is used. The value of 1,000 operators is a design goal that no measurement supports.

Resources of the smallest installation

Target. The repository states no minimum for memory, processors or disk, and no test finds one.

What the code does. The groups in core/setups/groups/ define the nodes of an installation. The table shows the nodes of each group. A published node has a public name; an internal node does not.

Group Published nodes Internal nodes
hosting-provider authelia, bind, garage, lldap, netbox, openbao, otel-collector, overview, postgresql, smtp, spire, telemetry, terraform-backend, valkey, windmill --
hypervisor incus --
operator mastodon, peertube, pixelfed authelia, grafana, lldap, smtp, valkey
  • The nodes of the group hosting-provider exist once for each hosting provider. The front end adds the nodes of the API and the panel; they are not in this table.
  • The nodes of the group operator exist for each operator deployment. A deployment has the nodes of the applications that the operator enables and the internal nodes that support them.
  • The root disk of an Incus instance has a default size of 50GiB (instanceRoot.size in core/components/hosting-provider/incus.nix), which Incus applies only when the storage pool can hold a quota.
  • The code of core sets no memory limit and no processor limit on an instance.

The part deployment-topology describes the nodes and where they run.

How it is tested. No test. The NixOS tests run each node in a virtual machine or a container with the resources of the test, which are not a statement about a production installation.

What it does not do. The code does not check that a host has sufficient resources before a deploy.

State lifecycle

This page tells what happens to the state of a running system when the code of the project changes, and when a service gets a new version. State is the data that a deploy does not make again: databases, buckets, secrets, the OpenTofu state, and the records of api/ and panel/.

The rule for each type of change

Type of change Mechanism What the mechanism does not do
An option of the operator configuration is renamed The rename table of api/, with the log core/renames-log.json It changes only the stored configuration. It moves no deployed object.
A value of the configuration changes The deploy writes the value into a configuration file or the environment of a unit, and the unit starts again It does not change a value that the application copied into its own state, for example a default of a new account
An OpenTofu resource gets a new address A moved block It does not change the object. It cannot serve an address with a key that is different for each deployment.
An object exists, but the state does not have it An import block It does nothing for an address that the state has.
A resource is dropped from the configuration The apply destroys the object. Some resources have a guard or a retire step. It does not delete an object that is not in a state.
An application is disabled The data stays. deleteData deletes it. Without deleteData, nothing is deleted. With it, the backup bucket stays for the grace period.
A deployment is destroyed tofu destroy of the operator graph, with destroy-time steps It does not delete the backups at once, the domain registration, or the records of api/ and panel/.
A service gets a new version systemd.stateMigrations, from the migrations.nix of the service It moves no object. It runs a schema command for some services only. It restores automatically for some services only.
The schema of api/ or panel/ changes Alembic revisions that api/ applies, Django migrations that panel/ applies, and a version record No migration goes back. The dump from before the upgrade is the only way back.

The sections below give the details and the files.

Two types of OpenTofu state

The code uses OpenTofu in two layers:

  • The deployment graphs are in core/effects/tf/. Their state is in the terraform-backend service of the hosting provider.
  • The on-node configurations are generated from Nix and applied by a unit on the node, for example core/resources/postgresql/provision.nix, core/resources/ldap/provision.nix, core/resources/garage/provision.nix and core/components/hosting-provider/openbao/runtime/module.nix. Their state is a terraform.tfstate file on the node.

In the two layers, the state is what connects the code to the objects. Thus a change that must move or adopt an object is a block in the OpenTofu configuration, or code written for that change.

An option is renamed

The panel and api/ store the configuration of a deployment as JSON. When an option of the operator configuration gets a new path, a stored configuration still has the old path.

core/lib/renames-for-source.nix makes a table of { from; to; } entries from the option tree: each rename that is declared with lib.mkRenamedOptionModule or lib.mkAliasOptionModule, and each rename of a key in a pinned upstream JSON Schema, which a settings-renames.json file beside the component declares. core/renames-log.json keeps the entries of earlier revisions, each with the since version that added it. api/package.nix puts the table in the package as renames.json, and its build fails for a rename that the log does not have.

core/renames-log.json has 0 entries.

Before api/ validates a stored configuration, _migrate_for_validation in api/src/fediversity_api/procedures.py moves each value from its old path to its new path (api/src/fediversity_api/migrate.py). A deployment row records the version of the log that its configuration agrees with (renames_version in api/src/fediversity_api/state.py), thus only the newer entries apply. A rename that cannot apply is reported as unmigrated, and the value stays where it is.

This mechanism changes only JSON. If the renamed option is part of the name of a deployed object, the next apply sees a different object: see the next sections.

A stored deployment also records the source of its modules. api/ stores a pin of its own revision for a deployment without a source, thus a rebuild of api/ does not change the schema of a stored deployment until its next deploy. See "Default-source baseline freeze" in api/docs/design/schema-and-sources.md.

A value changes

For most values of the operator configuration, and for the environment variables of api/ and panel/, the deploy writes the value into a configuration file or into the environment of a unit. When the file or the environment changes, the unit starts again. Thus the next deploy applies the new value.

Some applications keep a value in their own state, and that value wins over the deployed value. The code prevents this for these values:

  • Pixelfed writes a setting into its config_cache table at the first read, and from then on the row wins. core/components/operator/pixelfed/node.nix sets ENABLE_CONFIG_CACHE = false, thus the value of the .env file applies.
  • PeerTube reads local-production.json in its configuration directory over the deployed configuration. Its web edit of the configuration writes that file. core/components/operator/peertube/node.nix turns off the web edit, and the preStart of peertube.service removes the file.
  • The secrets of the operator are in OpenBao KV (api/src/fediversity_api/operator_secrets.py). When the rename table renames the field of a secret, a submit copies the value from the old name to the new name. When the operator clears a secret, the next deploy deletes its KV entry with all its versions (core/effects/tf/operator-provision/kv-clear.tf).

These values apply only to new objects, or stay with the application, by design:

Value What keeps the old value
PeerTube user.video_quota*, defaults.*, instance.default_nsfw_policy, user.default_channel_name PeerTube copies the value into a user, channel or video when it makes it. The new value applies to new objects only.
PeerTube admin.email PeerTube makes the root account one time, with this address. The contact address of the instance follows.
Mastodon AUTHORIZED_FETCH A value wins and locks the toggle of the admin. With no value, Setting.authorized_fetch in the database decides.
Mastodon HCAPTCHA_* The keys follow. The captcha is on only when the keys are set and an admin turns it on.
Authelia default_2fa_method, totp.*, regulation.ban_time Authelia keeps the method, the enrollment and the ban of each user.
api/ OPENBAO_KV_MOUNT, OPENBAO_KV_DEPLOYMENTS The entries stay at the old path, and api/ reads the secrets as not set. A change needs a move of the entries by hand.
api/ API_DATABASE_URL For an empty database, import_sqlite_state in api/src/fediversity_api/state.py copies the SQLite file in, not the data of the database before. A move between two PostgreSQL databases is by hand.
panel/ OIDC_ISSUER The actors of the audit records and the UserSocialAuth rows keep the old issuer.

The docs build generates this table and the list above from core/docs/md/technical-design/state-lifecycle.nix. The build fails when an option of a row is not in core/components/operator/operator-facing-options.csv, or when a file that a row names no longer has the code that the row describes.

A resource gets a new address

When the address of a resource changes and nothing else is done, OpenTofu destroys the object of the old address and creates an object at the new address. For an object that holds data, the new object is empty.

A moved block prevents this: it tells OpenTofu that the object of the old address is the object of the new address. The tree has these:

  • core/effects/tf/operator-provision/garage.tf has the only moved block in a .tf file, for a terraform_data resource with a new name.
  • core/lib/tf-labels.nix (movedBlocks) generates the moved blocks of the on-node configurations, from the labels <prefix>_<key> that the states of deployed nodes have, to the labels of the current code.
  • core/resources/garage/provision.nix changes its state file with jq before the apply, because a moved block between two providers needs a provider that reads the old type.
  • core/components/hosting-provider/incus.nix repairs its state file in the preStart of the unit, before the moved blocks apply.

The limits:

  • A moved block and a removed block accept only static addresses. The comment before garage_bucket.bucket in core/effects/tf/operator-provision/garage.tf gives the result: a resource whose for_each keys change with each deployment cannot get such a block.
  • No .tf file in the tree has a removed block.
  • A moved block does not rename the object itself. A database, a bucket or a node with a new name is a new object.

An object exists, but the state does not have it

An import block adopts the object, so that the apply does not create it again:

  • The on-node configurations of PostgreSQL, lldap and garage adopt an object that exists and is not in the state, for example a database that ensureDatabases made.
  • core/effects/tf/incus/operator/registrar.tf adopts a domain claim in OpenBao, a domain that is in the registrar account already, and a name server group.

notes/forward-recovery.md gives the rules for an apply that failed: there is no rollback, and the same apply runs again.

A resource is dropped

When a resource leaves the configuration, the next apply destroys its object. This is the default of OpenTofu in the two layers:

  • In an on-node configuration, an object that leaves the configuration is dropped. For PostgreSQL, a grant is revoked, and a database is dropped with its data (core/resources/postgresql/provision.nix).
  • In a deployment graph, the destroy-time provisioners of a resource release the objects that are not in the state, for example DNS records.

Some resources have a different rule:

Resource Rule File
A garage bucket of a deployment lifecycle { destroy = false }: OpenTofu only removes it from the state. terraform_data.retire_bucket then empties and deletes the bucket, or keeps a *-backups bucket for the grace period. core/effects/tf/operator-provision/garage.tf
A domain registration prevent_destroy: a plan that destroys or replaces it is refused. A replacement is a second registration, not a rename. The registration has a state key of its own. core/effects/tf/openprovider/domain.nix, core/effects/tf/inwx/domain.nix, core/effects/tf/porkbun/domain.nix
The secrets engine mounts of OpenBao prevent_destroy, set through resourceMeta. core/components/hosting-provider/openbao/runtime/module.nix, core/components/hosting-provider/openbao/database-init.nix, core/components/hosting-provider/openbao/pki-init.nix, core/components/hosting-provider/openbao/ssh-certificates.nix, core/components/hosting-provider/openbao/valkey-engine.nix
The cert auth mount of OpenBao in the state of spire-cert-auth A removed block with lifecycle.destroy = false: the state forgets the mount, and openbao-spire-auth adopts it by its path. core/components/hosting-provider/openbao/spire-auth.nix
An lldap user or membership in retain A removed block with lifecycle.destroy = false: the object leaves the state and stays in lldap. When it comes back to the configuration, its import block adopts it again. core/resources/ldap/provision.nix
The Incus network ACL of an operator deployment in the module bridge_acl A removed block with lifecycle.destroy = false: the state forgets the ACL and Incus keeps it. The hypervisor declares one ACL for each slot of the bridge, and a deploy credential that is limited to the project of the deployments cannot delete an ACL. core/effects/tf/incus/operator/bridge-isolation.tf
The node that keeps the OpenTofu state refuse_backend_replace: an apply that deletes or replaces it is refused with status 3 before it changes anything. A plan gets a warning. core/effects/tf/common/bootstrap-apply.sh

The docs build generates this table. It fails when a .nix or .tf file below core/ has prevent_destroy = true or destroy = false and no row names the file, and when a file of a row no longer has that code.

Gaps:

  • An object that is in no state has no deletion path. The on-node configuration of PostgreSQL says so for its own objects: an object that is in neither the configuration nor the state is not touched, thus a name that left the configuration before the first apply stays.
  • Only the node of the state backend has a guard against a replacement. For each other node, a change that replaces the node gives a new node, and the data on the old root disk is gone unless a volume or a backup holds it.

An object outside OpenTofu is dropped

A script or a unit makes some objects, not an OpenTofu resource. Such code follows a drop only when it lists the objects that exist, compares them with the objects that the code states, and removes the others. These do:

Object What removes it File
The Incus trust of a deploy runner The trusted certificates and pending tokens with no deploy runner of the host core/components/hosting-provider/incus-deploy-tokens.nix
A maddy account A compare of the accounts with the configuration core/resources/smtp/maddy-accounts.nix
A NetBox service user and its token The users of the group fediversity-managed that the deploy does not state core/components/hosting-provider/netbox-provision-token.py
An OpenBao token role A compare with the roles of the configuration core/components/hosting-provider/openbao/runtime/tokens.nix
A credential file on a node phase_prune_credentials core/effects/common/deploy.sh
A KV entry below vars/secret/deployer/ The keys that the code does not write, with all their versions core/components/hosting-provider/openbao/deployer-kv-prune.nix
A Windmill API token The unit deletes each token that it replaces core/components/hosting-provider/windmill/api-token.nix
A Windmill script The scripts of the folder that the module does not declare, with each version core/components/hosting-provider/windmill/script-sync.nix
A PeerTube plugin The plugin rows and node_modules entries that the deploy does not state core/components/operator/peertube/oidc-plugin-install.nix
A DNS mail record A destroy-time step for each name core/effects/tf/dns-lane/mail-records.tf
A Valkey user or rule ACL SETUSER ... reset, and the users that the configuration does not state core/components/shared/valkey/valkey.nix
A Grafana alert rule The retired UIDs in deleteRules core/components/hosting-provider/telemetry.nix, core/components/hosting-provider/telemetry/retired-alert-rules.nix
The restic repository of a backup that stops while the bucket stays, of core/components/operator/acme-backup.nix, core/components/operator/local-cache-backup.nix The repository is deleted when its newest object is older than the retention period core/resources/garage/retired-repository.sh
The registrar auth code of a domain transfer drop_stale_domain_transfer, at a submit whose configuration registers a different domain, or no domain api/src/fediversity_api/migration_kv.py

The docs build generates this table. It fails when a file of a row no longer has the code that removes the object. The list of backups with a sweep comes from the calls of mkRetiredRepository. The build does not find a new removal that has no row.

Gaps:

  • When the rename table renames the field of a secret of the operator, the KV entry of the old name stays.
  • When a role leaves core/components/operator/mastodon/role-groups.nix, the holders keep it on Mastodon (core/components/operator/mastodon/ldap-roles.rb). No path of the configuration gets to this.
  • The sweep of retired restic repositories serves only the backups in the table above. Other restic repositories of a backup that stops stay, for example the repository of the dump before a PostgreSQL upgrade.

An application is disabled

The option deleteData of an application is in core/components/sharedOptions.nix. Its default is false.

  • When enable becomes false and deleteData is false, the data stays: the database, the buckets, the generated secrets and the cache. A new enable uses the data again.
  • When enable is false and deleteData is true, the next deployment deletes the data. purgedApplications in core/setups/wiring/node-selection.nix finds these applications. core/setups/wiring/supporting-requests.nix leaves their requests out, thus the operator graph deletes their buckets, databases and keys, and the valkey node deletes their cache. core/effects/tf/operator-provision/openbao.tf deletes their KV directories (purged_kv_dirs, from core/setups/wiring/purged-kv.nix).
  • A *-backups bucket stays for the grace period, as at a destroy.

The operator documentation of the option is in the operator configuration, and its role for erasure is in Data protection.

A deployment is destroyed

A destroy of a deployment runs tofu destroy on the operator graph. It removes the nodes. core/effects/tf/operator-provision/postgresql.tf drops the databases and the login roles of the deployment on the shared PostgreSQL node: their names have the namespace of the deployment as a suffix, and this graph is the only actor that runs again when the deployment ends. terraform_data.after_instances in core/effects/tf/operator-provision/openbao.tf deletes what remains below the KV prefix of a namespaced deployment after the instances are gone. terraform_data.retire_bucket empties and deletes each bucket that is not a backup bucket.

The destroy keeps this state:

State Why How long
Each <app>-backups bucket, with the new alias retained-<YYYYMMDD>-<id prefix> A restore stays possible, and the name of the bucket is free for a new deployment. The grace period: fediversity.garage.server.retainedBackups.gracePeriodDays of the garage node, default 7 days
The KV entries that the bucket cannot be read without, and a retention record in OpenBao Without the restic password, the backup cannot be read. The record tells the garage node what to delete. The same grace period. core/resources/garage/sweep-retained-buckets.sh then deletes them with the bucket.
The domain registration prevent_destroy. A registration that is lost cannot be made again as before. Until the registration ends at the registrar
The row of the deployment in the panel, with deleted_at api/ can still bill the usage (panel/src/panel/models.py). Until the account of the operator is removed
The builds and the audit_event rows in the database of api/ The usage periods of a destroyed deployment stay available, and the audit log records each deployment_delete. See Data protection
An object of a destroy step that failed The destroy-time provisioners run with on_failure = continue, so that a destroy of a deployment that is not complete can finish. Until a person removes it. The run lists the object and exits with status 4.

The check core/checks/integrations/destroy-reports-left-behind covers the last row. The operator steps for a retained bucket are in Know what a destroy keeps. An export for a move to a different hosting provider must be complete before the destroy: no key of the operator can read a retained bucket.

A destroy of all nodes of the hosting provider also destroys the node that keeps the OpenTofu state. Thus the run first moves the state to a local file (core/checks/integrations/destroy-detaches-state).

A service gets a new version

Each service that keeps state has a systemd.stateMigrations.<name> entry, in most cases in a file migrations.nix beside the service. The module is nix/contracts/nixos/modules/system/state-migrations.nix. An entry gives the version of the service in the current code. A unit <name>-migrate-pre runs before the service, compares that version with the version in a record file on the node, and decides:

flowchart TD
  start["The unit `<name>-migrate-pre` starts"] --> record{"Is there a record of the version?"}
  record -- "no" --> fresh["Record the version. Run `onFreshInstall` if the entry has one and the state is new."]
  record -- "yes, the same version" --> none["Do nothing"]
  record -- "yes, a later version than the code" --> down{"What is the value of `onDowngrade`?"}
  down -- "refuse" --> stop["Exit with status 78. The service stays stopped."]
  down -- "warn or ignore" --> older["Record the older version. Run nothing."]
  record -- "yes, an earlier version than the code" --> skip{"Is the step larger than `maxSkip` permits?"}
  skip -- "yes" --> stop
  skip -- "no" --> backup["Run `backup`, then `steps` and `onUpgrade`"]
  backup --> service["The service starts"]
  service --> post["The unit `<name>-migrate-post` runs `onUpgradePost` and records the version"]

The rules of the module:

  • The backup. backup runs one time for each upgrade, before each other command of the upgrade.
  • The downgrade. onDowngrade is refuse by default. Exit status 78 tells systemd not to start the unit again. With warn or ignore, the unit records the older version and runs nothing: these are services that also read the state of a later version.
  • The largest step. maxSkip is not set by default, and then each step to a later version is permitted. With major, a step across more than one major release is refused. With minor, a step across more than one minor release is also refused.
  • The restore. When the entry has a restore command, the unit records an upgrade that is not complete. A deploy of the version from before the upgrade then runs restore. A deploy of a third version is refused. Without restore, the way back is a manual restore of the backup.
  • No record. When freshInstallTest is null and the node has no record, the unit records the version and does not migrate. Thus the first deploy of an entry onto state that exists does not make a backup.

What an entry does not do:

  • It moves no object and changes no OpenTofu state.
  • In most entries, it runs no schema command: the service migrates its own schema when it starts. The column "Upgrade command" of the table shows the entries that do run a command. For example, core/components/operator/mastodon/migrations.nix runs the database migrations of Mastodon, and the post-deployment migrations only after the services are up again.
  • No entry has a command for one specific version: the list steps is empty in each entry.
  • An entry whose version is a release series, for example 2.5, sees no change at a patch release. Thus a patch release makes no backup.

These files below core/components and core/resources define an entry. core/setups/nixos/postgresql.nix and core/setups/nixos/mysql.nix define the entries of PostgreSQL and MariaDB.

  • core/components/dev/forgejo/migrations.nix
  • core/components/hosting-provider/audit-shipper-migrations.nix
  • core/components/hosting-provider/bind-migrations.nix
  • core/components/hosting-provider/netbox/migrations.nix
  • core/components/hosting-provider/openbao/migrations.nix
  • core/components/hosting-provider/spire/migrations.nix
  • core/components/hosting-provider/telemetry/migrations.nix
  • core/components/hosting-provider/terraform-backend/migrations.nix
  • core/components/hosting-provider/windmill/migrations.nix
  • core/components/operator/mastodon/migrations.nix
  • core/components/operator/peertube/migrations.nix
  • core/components/operator/pixelfed/migrations.nix
  • core/components/shared/authelia/migrations.nix
  • core/components/shared/lldap/migrations.nix
  • core/components/shared/valkey/migrations.nix
  • core/resources/garage/migrations.nix
  • core/resources/smtp/maddy-migrations.nix

The table shows the entries of the nodes of core with the default applications and the configuration of the tests. The docs build generates it from config.systemd.stateMigrations of each node of the groups hosting-provider and operator of core/testers/groups.nix. The column "Backup before the upgrade" is written by hand in core/docs/md/technical-design/state-lifecycle.nix, and the build fails when it is not in agreement with the backup script of the entry. A row is one entry, or the entries with the same name and the same values on more than one node. The version of each entry is the version of the package, or its release series (garage, for example), or its major version (PostgreSQL). An entry with a restic snapshot makes it on a node that has the offsite backup for that service. Without the offsite backup, the entry makes a local copy or a dump, or no backup.

Entry Nodes Version Largest step Downgrade Backup before the upgrade Automatic restore Upgrade command
authelia hosting-provider/authelia, operator/authelia 4.39.28 no limit refuse A restic snapshot yes no
bind hosting-provider/bind 9.20.30 no limit warn A restic snapshot yes no
garage hosting-provider/garage 2.5 no limit refuse A local copy no no
grafana hosting-provider/telemetry 13.3.0 no limit refuse A restic snapshot yes no
lldap hosting-provider/lldap, operator/lldap 0.6.3 no limit refuse A restic snapshot yes no
maddy hosting-provider/smtp, operator/smtp 0.9.6 no limit warn A local copy no no
mastodon operator/mastodon 4.7.4 one minor release refuse A restic snapshot yes yes
netbox hosting-provider/netbox 4.7.3 one major release refuse A restic snapshot yes no
openbao hosting-provider/openbao 2.7 no limit refuse A local copy no no
peertube operator/peertube 8.3.2 no limit refuse A restic snapshot yes no
pixelfed operator/pixelfed 0.12.9 no limit refuse A restic snapshot yes yes
postgresql hosting-provider/postgresql 18 no limit refuse A restic snapshot of all clusters no yes
redis-<name> hosting-provider/valkey, operator/valkey 9.1 no limit warn A restic snapshot yes no
spire-server hosting-provider/spire 1.15 no limit warn A local copy no no
terraform-backend hosting-provider/terraform-backend 0.2.4 no limit refuse A restic snapshot yes no
windmill hosting-provider/windmill 1.839.0 no limit refuse A restic snapshot yes no

These nodes of that configuration have no entry: hosting-provider/otel-collector, operator/grafana.

The table does not read the overview tag, the hypervisor and the development hosts. These entries are not on a node of that configuration, thus the table does not show them:

  • fediversity-audit-shipper of core/components/hosting-provider/audit-shipper-migrations.nix. It refuses a downgrade and makes a local copy of its state directory.
  • forgejo of core/components/dev/forgejo/migrations.nix.
  • mysql of core/setups/nixos/mysql.nix, for a node that runs MariaDB.

Gaps:

  • The postgresql entry itself does not limit the step. The guard of the PostgreSQL module, below, permits only a step of one major version.

PostgreSQL has one more rule. Each major version keeps its cluster in a directory of its own, thus a new major would start with an empty cluster. core/setups/nixos/postgresql.nix sets the major in the code. When the major goes up by one, its guard runs pg_upgrade --copy from the old cluster. For each other change of the major, it keeps PostgreSQL stopped with exit status 78. MariaDB has a rule of its own in core/setups/nixos/mysql.nix: the module runs mariadb-upgrade after each start of the server, because an upgrade is complete only after that command.

The maintainer steps for an update, and the reasons for each service, are in Update nixpkgs. Each entry has a check in core/checks/integrations/, for example garage-series and openbao-series.

The schema of the api or the panel changes

The two components keep their records in a PostgreSQL database of the hosting provider. Each applies its own migrations, and each has a systemd.stateMigrations entry that only records the version, refuses a downgrade, and makes the backup.

Component Entry Version Migrations Definition
api/ fediversity-api 12 Alembic revisions in api/src/fediversity_api/state_migrations/versions api/nix/component/migrations.nix
panel/ panel 9 Django migrations in panel/src/panel/migrations panel/nix/component/migrations.nix
  • api/ applies the Alembic revisions when it opens its store (_migrate in api/src/fediversity_api/state.py). A PostgreSQL advisory lock keeps a second process out of the same migration. api/ refuses a database at a revision that its release does not know. The revisions only go forward.
  • panel/ runs manage migrate in the preStart of panel.service (panel/nix/configuration.nix), each time the package changes. Django does not refuse a database that later migrations changed, thus the version record is the only guard against a downgrade. The panel has no migrations that go back.
  • The version of each entry is the number of the last migration file. Thus the version changes only when the schema changes. For the panel, the migrations of the Django apps from nixpkgs are not in the version.
  • The backup comes from mkUpgradeBackupFor in core/components/operator/app-resources.nix: on a node with the offsite backup of core/components/shared/postgres-backup.nix, a dump of the database before the upgrade. Without that backup, the entry makes no backup.
  • The largest step is not limited: the entries do not set maxSkip. Alembic and Django apply each migration between the two versions in sequence.

The two entries are not in the table of the section before, because the nodes of api/ and panel/ are not nodes of core.

An effect fails

An effect can fail, or a person or a timeout can stop it, at any point. No effect goes back to the state from before the run. Each effect is recovered forward: run it again. For a deployment graph, notes/forward-recovery.md gives the rules and tells how to read a failure.

Effect When it fails or is stopped Run again Automatic retry or wait Objects left behind File
A deployment graph (core/effects/tf/, core/effects/k8s/) The state records the steps that are done. A dead run can leave its lock. A state write that the backend rejects is kept as errored-<time>.tfstate. Yes: the same apply continues from the state (notes/forward-recovery.md). The client of the HTTP backend tries a request again 2 times, after 1 to 30 s. Before an apply, the run waits for the backend at most 1800 s. With TF_RECLAIM_OWN_LOCK=1, the run releases a lock of its own identity. Yes: a destroy-time step that fails gives status 4 core/effects/tf/common/bootstrap-apply.sh, core/effects/tf/common/effect.nix
An Incus host of a graph As for a deployment graph Yes The step waits for incus exec in the new instance 30 times, each 5 s apart, and then for its address 30 times, each 2 s apart. As for a deployment graph core/effects/tf/incus/host/await-incus.sh
A Kubernetes rollout A rollout that does not become ready is undone, and the apply fails Yes The step waits for the rollout at most 600 s. The copy of an image is tried 3 more times. As for a deployment graph core/effects/k8s/effect.nix
A deploy over SSH (ssh) The node runs the old or the new configuration Yes: it copies the closure and switches again It waits for the target 30 times, each 5 s apart. When another switch holds the lock, it tries again 60 times, each 5 s apart. No core/effects/common/deploy.sh, core/effects/ssh/run.sh
A deploy into an Incus instance (incus) The node runs the old or the new configuration. After a failed switch, /sbin/init points to the old closure again. Yes: it copies the closure and switches again It waits for the target 30 times, each 5 s apart No core/effects/common/deploy.sh, core/effects/incus/run.sh
An install (nixos-anywhere) The disks can be wiped, or hold a part of the install Yes, but a run that installs wipes the disks again. A target that runs the expected system is skipped, and its disks stay. None No core/effects/nixos-anywhere/install.sh
An export of a migration (migration) The services that the run stopped start again Yes: it makes a new snapshot None No core/effects/migration/run.sh
A restore of a migration (migration-restore) The restore can be partly done It restores again. The script does not say that this is safe. It waits for the instance to boot at most 900 s No core/effects/migration-restore/run.sh
An operation of an application (app-operation) A deploy that restarts the node makes the operation fail It depends on the command None. The command stops after 300 s. No core/effects/app-operation/run.sh
A retry of the objects left behind (left-behind) A removal that fails keeps its record, and the other removals continue Yes: each removal has the guards of its step None. It is itself the retry, and a person starts it from api. Keeps the records that it could not remove core/effects/left-behind/retry.sh
A sync of a zone (octodns) The zone can hold a part of the changes Yes: a sync writes only the difference None No core/effects/octodns/effect.nix
A node effect that api starts (an export, a restore, an operation, a retry) api cancels a job that is not done after 3600 s, and the effect fails As for the effect None As for the effect api/src/fediversity_api/node_effect.py, api/src/fediversity_api/windmill.py

The docs build generates this table. It fails when a file of a row no longer has the code that does what the row says. The build does not find a new retry or a new effect that has no row.

Gaps:

  • No effect tries a failed run again by itself. The retries in the table are inside one run.
  • A restore of a migration and some operations of an application do not say that a second run is safe.

What this page does not cover

State register

This part lists each store of state of the software: where it is, which component owns it, whose personal data it holds, how long it keeps the data, how it is protected, and how to erase data from it. It gives the facts for the record of processing that the data protection page assigns to the operator and to the hosting provider. It is not legal advice, and it gives no legal classification.

How the register is made

The tables on this page are generated at the documentation build from two inputs:

  • The declaration core/docs/md/technical-design/state-register-stores.nix. It gives, for each store, the purpose, the personal data, the retention, the encryption, the backup and the procedure to erase, with the files that these statements come from.
  • The code. The build reads the databases, the buckets, the valkey namespaces and the persistentPaths that the components request, the tables that the migrations of api/ create (api/src/fediversity_api/state_migrations/versions/), and the models of the panel (panel/src/panel/models.py). The components are those of an example deployment with each application enabled.

The build compares the two inputs. The check core-docs fails when:

  • an item in the code is in no store of the declaration,
  • the declaration names an item or a source file that does not exist,
  • a component owns no store and is not in the list of components without a store.

The check sees only the items that the code declares as data. It does not see a state directory that a module sets on a node, for example /var/lib/maddy. The declaration gives these paths by hand.

"Open" in a table means that no source in the tree gives the fact. The hosting provider finds it in its setup or in the upstream documentation, see the unknowns of the data protection page.

Which store holds which

Some stores are inside other stores. A statement about the outer store, for example its encryption, also applies to the stores inside it.

flowchart TD
  pools["Incus storage pools on the hypervisor"]
  nodes["Paths on the nodes"]
  postgresql["PostgreSQL server"]
  garage["Garage"]
  databases["Databases of the applications and the services"]
  buckets["Media and telemetry buckets"]
  backups["Backup buckets"]
  pools --> nodes
  nodes --> postgresql
  nodes --> garage
  postgresql --> databases
  garage --> buckets
  garage --> backups
  databases -- "backup" --> backups
  nodes -- "backup" --> backups

Location and owner

The column "Declared in the code" gives the items that the build found in the code for the store. The databases of the panel and of api/ and their backup buckets are not in the example deployment, thus the build does not check them.

Store Owner Node and path Purpose Declared in the code
api/ database api The api database on the PostgreSQL node, or a SQLite file. The deployments, their builds and operations, their domains, the SBOMs of their nodes, and the objects that a destroy-time step did not remove. table of api/: deployment, build, operation_run, zone_follow, domain_transfer, domain_event, domain_contact, sbom_component, node_sbom, node_sbom_component, left_behind
Application databases mastodon, peertube, pixelfed One database for each application of a deployment, on the PostgreSQL node of the hosting provider. The records of the application. bucket: mastodon-backups, peertube-backups, pixelfed-backups; database: mastodon, peertube, pixelfed
Application files on the node peertube, pixelfed /var/lib/peertube on the PeerTube node and /var/lib/pixelfed/storage/app on the Pixelfed node. The files that the application does not keep in a bucket. --
Application media buckets mastodon, peertube, pixelfed Buckets on the Garage node of the hosting provider, with a prefix for each operator. The media that the application serves. bucket: mastodon, peertube-videos, peertube-playlists, pixelfed
Audit records api The audit_event and audit_chain_anchor tables of the api database. api/ also writes each event to the journal, and the tenant audit of Loki keeps that copy. The record of who did which action on a deployment. table of api/: audit_event, audit_chain_anchor
Authelia authelia The authelia database on the PostgreSQL node, and the authelia namespace of valkey for the sessions. Each group has its own Authelia node. The single sign-on of a group of nodes. bucket: authelia-backups; database: authelia; valkey namespace: authelia
Backups each node The *-backups buckets on the Garage of the hosting provider. A group can also copy the backups to a target outside the site. The restore of a service. --
DNS zones bind /var/lib/named on the BIND node. The DNS zones that the hosting provider serves. bucket: bind-backups
Garage garage /var/lib/garage on the Garage node of the hosting provider, with the metadata snapshots in /var/lib/garage/snapshots. It holds each bucket of this register. The object storage, and the target of the backups. --
Grafana of the hosting provider telemetry A SQLite file on the telemetry node. The accounts of the OIDC logins and their preferences, the service accounts, the silences, the annotations and the dashboards that are not provisioned. bucket: grafana-backups
Grafana of a deployment grafana The file grafana.db in a runtime directory below /run on the Grafana node of the deployment. The local records of the Grafana that shows the metrics of a deployment to its operator. --
Incus storage pools incus The storage pools of fediversity.incus.storagePools on the hypervisor. The root file systems of the nodes. Each store of this register that is a path on a node is in a pool. --
lldap lldap The lldap database on the PostgreSQL node. Each group has its own lldap node. The directory of the users and the groups of a group of nodes. bucket: lldap-backups; database: lldap
Journal and access logs each node The journal of each node, and the access log of nginx on each node that runs nginx. The debugging of a node. --
Mail smtp /var/lib/maddy on the SMTP node of the group. The queue of the mail that the applications and the shared services send. --
NetBox netbox The netbox database on the PostgreSQL node. The address management (IPAM) of the hosting provider. bucket: netbox-backups; database: netbox
OpenBao openbao The raft storage in /var/lib/openbao/raft on the OpenBao node. The secrets of an instance are below the KV path secret/data/vars/secret/<instance>/. The secrets that the components generate and the secrets that the operator brings. bucket: openbao-backups
Panel database panel The panel database on the PostgreSQL node. A panel without a DATABASE_URL uses a SQLite file below /var/lib. The accounts of the operators, their settings and their deployments. model of the panel: Deployment, DomainContact, OperatorSettings
PostgreSQL server postgresql The PostgreSQL node of the hosting provider. It holds each database of this register that is not SQLite. The database server that the components share. bucket: postgresql-backups
SPIRE spire /var/lib/spire-server on the SPIRE node. The identities of the workloads. --
Telemetry (Loki, Tempo, Mimir) telemetry The loki, tempo and mimir buckets on Garage. The logs, the traces and the metrics of the nodes. bucket: loki, tempo, mimir
OpenTofu state backend terraform-backend /var/lib/private/terraform-backend on the node of the state backend. The OpenTofu states of the deployments. bucket: terraform-backend-backups; path: /var/lib/private/terraform-backend
Valkey namespaces valkey /var/lib/redis-<namespace> on the valkey node of the group. An application of a group that has no valkey node uses a local redis on its own node. The cache and the job queue of an application. valkey namespace: mastodon, peertube, pixelfed, valkey
Windmill windmill The windmill database on the PostgreSQL node. The jobs that deploy. bucket: windmill-backups; database: windmill

Personal data and retention

The personal data of each log, trace and audit channel, with its readers and its controls, is in Logs, traces and audit records.

The retention settings that an operator can set for an application are in Personal data per application.

Store Personal data, and of whom Retention of the live store
api/ database Of the operators: the configuration of each deployment, the build logs, the output and the actor of each operation, and the identifier and the handle of each domain contact. services.fediversity-api.logRetentionDays for the build logs and the runs of the operations: 30 days by default, 0 keeps them. The operation_run table keeps at most the last 100 runs of each deployment. No job removes old rows of the build table. A row of the left_behind table stays after its deployment is gone, until a retry removes the object or an operator forgets the row. For the other tables the code sets no period.
Application databases Of the users of the application: the accounts and their email addresses, the posts, the direct messages, the follows and the sessions. For PeerTube also the videos and their metadata, the comments and the watch history. The applications also keep content that they get from other instances. Until the user or a moderator deletes the data. The code sets none of the retention settings of the applications.
Application files on the node Of the users of the application: for example avatars, thumbnails, captions, profile headers and custom emoji. /var/lib/peertube also holds the logs of PeerTube. Until the application deletes the file. The code sets no period.
Application media buckets Of the users of the application: the uploads, for example images, videos and avatars. Mastodon also keeps cached media of other instances. Until the application deletes the object. The code sets none of the media retention settings of the applications.
Audit records Of the operators and the administrators: the actor of each action. services.fediversity-api.auditRetentionDays for the table and services.fediversity-telemetry.auditRetentionDays for the tenant audit, each 365 days by default.
Authelia Of the users of the group: the consents, the second factors, the identity verification tokens, the OIDC state and the sessions. The code sets no period. The code also does not set the session lifetime, the inactivity limit and the regulation. The operator can set them.
Backups That of the store that each backup is from. See its row. See the backup of each row. After a destroy, a *-backups bucket stays for the grace period of Garage.
DNS zones None from the software: the records hold the names and the addresses of the services. Open: no source in the tree gives it.
Garage That of the buckets on it. See their rows. That of the buckets on it. Garage keeps one copy of each object (replication_factor = 1).
Grafana of the hosting provider Of the administrators: the accounts that the first OIDC login makes. The code sets no period.
Grafana of a deployment Of the operators: the accounts that an OIDC login makes. Until the next restart of the service.
Incus storage pools That of the stores on the nodes. See their rows. Until a destroy removes the node.
lldap Of the users of the group: the user names, the email addresses, the names, the avatars and the group memberships. The first administrator gets the email of the configuration. Until a member of lldap_admin deletes the user.
Journal and access logs Of the users, the operators and the administrators: client addresses and user names (sshd, nginx errors, Authelia), email addresses (maddy), the audit lines of OpenBao, and what each application writes to its log. The journal: 30 days and at most 512 MB (MaxRetentionSec, SystemMaxUse). The access log of nginx: logrotate keeps 26 weeks.
Mail Of the senders and the recipients: the addresses, the headers and the content of the mail that passes, for example password resets and notifications. The code sets no retention on the queue.
NetBox Of the administrators: their accounts. If the IPAM records hold personal data is open. Open: no source in the tree gives it.
OpenBao Of the administrators: the names that OIDC logins give them. If a secret is personal data is open. Until deleteData or a destroy removes the KV entries. The code sets no period.
Panel database Of the operators: the accounts, with the email address and the user name from the OIDC login, the sessions, the domain contacts of the operator with their labels and handles, and the deployments with their configuration and the domain contact of each role. The panel keeps the deployments that the operator deleted, for the billing. Until a panel administrator removes the account. The session lifetime is the default of Django, and no job removes expired sessions.
PostgreSQL server That of the databases on it. See their rows. That of the databases on it.
SPIRE Open: no source in the tree gives it. Open: no source in the tree gives it.
Telemetry (Loki, Tempo, Mimir) A copy of the journal of each node of the hosting provider, and the journal lines of priority warning and higher of each node of an operator deployment. See the row of the journal for the personal data. services.fediversity-telemetry.retentionDays, 90 days by default.
OpenTofu state backend Of the domain contacts: the state with the key <base>-<contact>-domain-contact holds the contact that an operator saves through api/: the name, the address, the telephone number and the email address, not hidden. If the other states hold personal data is open. The code sets no period. The state of a contact stays until DELETE /domain_contacts/{contact_id} of api/ erases it.
Valkey namespaces Of the users of the application: what the application caches, for example queued jobs and timelines. The code sets no retention on the cache.
Windmill Of the administrators: their accounts. Of the operators: the arguments, the result and the log of each job, which are the configuration of a deployment, its deploy log and the SBOMs of its nodes. Windmill removes a completed job after services.fediversity-windmill.jobRetentionDays, 30 days by default.

Encryption, backup and erasure

The table gives the encryption that the software adds to a store. The encryption of the disks is not in the code. The contract templates state that the disks of the hypervisors are not encrypted.

The procedures of the hosting provider are in the hosting provider guide: Know how long the backups keep personal data, Erase personal data and Know what a destroy keeps.

Store Encryption at rest Backup Procedure to erase
api/ database None in the code. A restic repository in the api-backups bucket. The backup keeps 30 days (--keep-within 30d). DELETE /deployments/{id} destroys a deployment. DELETE /domain_contacts/{contact_id} removes the handle of a contact. DELETE /deployments/left-behind/{entry_id}/{deployment_id} forgets one left-behind object.
Application databases None in the code. A restic repository in the <app>-backups bucket. The backup keeps 30 days (--keep-within 30d). The account deletion of the application. To erase all data of an application, set enable to false and deleteData to true, or destroy the deployment.
Application files on the node None in the code. A restic repository in the peertube-backups or the pixelfed-backups bucket. The backup keeps 30 days (--keep-within 30d). Open: no source in the tree gives it.
Application media buckets None in the code. The media backup, in the prefix media/ of the <app>-backups bucket. It keeps 30 days of history by default (fediversity.mediaBackup.instances.<app>.retentionDays). The application deletes the object. media-backup-<app> erase <bucket> <key> also removes the object from the media backup and from its history.
Audit records None in the code. With the api database, in the api-backups bucket. The backup keeps 30 days (--keep-within 30d). Open: no source in the tree gives it.
Authelia Authelia encrypts its database with a storage key (storageEncryptionKeyFile). A restic repository in the authelia-backups bucket. The backup keeps 30 days (--keep-within 30d). Open: no source in the tree gives it.
Backups The media backups and the restic backups are encrypted. None in the code. Garage has no backup of its objects. The restic backups have no erase command. An erased record stays in them until they expire. The media backup has media-backup-<app> erase.
DNS zones None in the code. The records of the apex zone on the primary, in the bind-backups bucket. The backup does not hold the zones of the operators, the zones of the challenge lanes or the DNSSEC keys (core/components/hosting-provider/bind/backup.nix). The backup keeps the snapshots of --keep-daily 7 --keep-weekly 4 --keep-monthly 6. Open: no source in the tree gives it.
Garage None in the code. None in the code for the objects. Garage makes snapshots of its metadata on the same node. A group can copy the backups to a target outside the site. See the rows of the buckets. A destroy keeps each *-backups bucket for a grace period (fediversity.garage.server.retainedBackups.gracePeriodDays, 7 days by default), then a timer deletes it.
Grafana of the hosting provider Grafana encrypts the secrets in its database with a secret key. The SQLite database, in the grafana-backups bucket. The secret key of the database is in OpenBao KV, not in the backup (core/components/hosting-provider/telemetry/backup.nix). The backup keeps the snapshots of --keep-daily 7 --keep-weekly 4 --keep-monthly 6. Open: no source in the tree gives it.
Grafana of a deployment None in the code. None. A restart of the service.
Incus storage pools None in the code. None for a pool. See the rows of the stores on the nodes. A destroy of a deployment removes its nodes.
lldap None in the code. A restic repository in the lldap-backups bucket. The backup keeps 30 days (--keep-within 30d). A member of lldap_admin deletes the user at https://users.<domain>/.
Journal and access logs None in the code. None. Loki keeps a copy, see the row of the telemetry. Open: no source in the tree gives it.
Mail None in the code. None. Open: no source in the tree gives it.
NetBox None in the code. Two restic repositories in the netbox-backups bucket: one for the database (core/components/hosting-provider/netbox/default.nix) and one for the files that users give to NetBox (core/components/hosting-provider/netbox/uploads-backup.nix). The backup keeps the snapshots of --keep-daily 7 --keep-weekly 4 --keep-monthly 6. Open: no source in the tree gives it.
OpenBao The unseal key is sealed to the TPM, and its copy on the disk is encrypted to age recipients. A raft snapshot and the unseal key, in the openbao-backups bucket. The unseal key is encrypted to the admin recipients (core/components/hosting-provider/openbao/backup.nix). The backup keeps the snapshots of --keep-daily 7 --keep-weekly 4 --keep-monthly 6. deleteData of an application deletes its generated secrets and the secrets that the operator brought. A destroy removes the KV entries of the deployment.
Panel database None in the code. A restic repository in the panel-backups bucket. The backup keeps 30 days (--keep-within 30d). A panel administrator removes the account. The panel first erases the domain contacts of the account, and queues the destroy of each deployment of the account.
PostgreSQL server None in the code. Each database has its own backup. Before an upgrade of the major of PostgreSQL, the server also makes a snapshot of its data directory in the postgresql-backups bucket. See the rows of the databases.
SPIRE None in the code. None. Open: no source in the tree gives it.
Telemetry (Loki, Tempo, Mimir) None in the code. None. Open: no source in the tree gives it.
OpenTofu state backend None in the code. The states and the kmsKey that encrypts them, in the terraform-backend-backups bucket. The key is encrypted to the admin recipients (core/components/hosting-provider/terraform-backend/backup.nix). The backup keeps the snapshots of --keep-daily 7 --keep-weekly 4 --keep-monthly 6. For a contact: DELETE /domain_contacts/{contact_id} of api/ deletes the customer in the registrar account and then the state. The call refuses while a domain of the account names the contact. A change of a name deletes the old customer, unless a domain names it.
Valkey namespaces None in the code. A restic repository for each namespace, in the cache backups bucket of the group. A local redis goes to the <app>-backups bucket. The backup keeps 30 days (--keep-within 30d). deleteData of the application deletes its cache.
Windmill None in the code. A restic repository in the windmill-backups bucket. The backup keeps the snapshots of --keep-daily 7 --keep-weekly 4 --keep-monthly 6. Open: no source in the tree gives it.

Components without a store

Component Why it has no store
otel-collector It sends each signal that it gets to the store of that signal on the telemetry node.
overview It serves a static page that lists the web UIs of the deployment.

Sources

Store Sources
api/ database api/src/fediversity_api/state.py, api/src/fediversity_api/left_behind.py, api/src/fediversity_api/state_migrations/versions/0001_baseline.py, api/src/fediversity_api/state_migrations/versions/0010_left_behind.py, api/nix/component/default.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Application databases core/components/operator/app-resources.nix, core/components/sharedOptions.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Application files on the node core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Application media buckets core/components/operator/app-resources.nix, core/resources/garage/default.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Audit records api/src/fediversity_api/state.py, api/src/fediversity_api/settings.py, api/nix/configuration.nix, core/components/hosting-provider/telemetry.nix, core/docs/md/guides/hosting-provider/logs.md, core/docs/md/guides/hosting-provider/backups.md
Authelia core/components/shared/authelia/default.nix, core/components/shared/authelia/database.nix, core/components/shared/authelia/sessions.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Backups core/components/operator/app-resources.nix, core/components/shared/postgres-backup.nix, core/docs/md/compliance/contract-templates.md, core/docs/md/guides/hosting-provider/backups.md
DNS zones core/components/hosting-provider/bind.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Garage core/resources/garage/server.nix, core/resources/garage/server-options.nix, core/resources/garage/metadata-snapshots.nix, core/resources/garage/migrations.nix, core/docs/md/guides/hosting-provider/backups.md
Grafana of the hosting provider core/components/hosting-provider/telemetry.nix, core/components/hosting-provider/telemetry/backup.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Grafana of a deployment core/components/operator/grafana/node.nix, core/docs/md/compliance/data-protection.md
Incus storage pools core/components/hosting-provider/incus.nix, core/docs/md/guides/hosting-provider/access.md
lldap core/components/shared/lldap/default.nix, core/components/shared/lldap/database.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Journal and access logs core/setups/nixos/common.nix, core/setups/nixos/nginx.nix, core/components/hosting-provider/node-logs.nix, core/docs/md/guides/hosting-provider/logs.md
Mail core/components/shared/smtp/default.nix, core/resources/smtp/maddy-migrations.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
NetBox core/components/hosting-provider/netbox.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
OpenBao core/components/hosting-provider/openbao/openbao.nix, core/components/hosting-provider/openbao/backup.nix, core/components/hosting-provider/openbao/deployer-policies.nix, core/components/sharedOptions.nix, core/docs/md/compliance/threat-model.md, core/docs/md/guides/hosting-provider/backups.md
Panel database panel/src/panel/models.py, panel/nix/component/default.nix, panel/nix/configuration.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
PostgreSQL server core/setups/wiring/postgresql-roster.nix, core/components/hosting-provider/postgresql/default.nix, core/docs/md/guides/hosting-provider/backups.md
SPIRE core/components/hosting-provider/spire/spire.nix, core/docs/md/guides/hosting-provider/backups.md
Telemetry (Loki, Tempo, Mimir) core/components/hosting-provider/telemetry.nix, core/components/hosting-provider/telemetry/object-store.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/logs.md, core/docs/md/guides/hosting-provider/monitoring.md, core/docs/md/guides/hosting-provider/backups.md
OpenTofu state backend core/components/hosting-provider/terraform-backend/state-dir.nix, core/components/hosting-provider/terraform-backend/backup.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md, core/docs/md/guides/hosting-provider/domain-transfers.md
Valkey namespaces core/components/shared/valkey/valkey.nix, core/components/shared/valkey/default.nix, core/components/sharedOptions.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md
Windmill core/components/hosting-provider/windmill/windmill.nix, core/components/hosting-provider/windmill/default.nix, core/docs/md/compliance/data-protection.md, core/docs/md/guides/hosting-provider/backups.md

Traceability of the capabilities

The deliverable is a technical design for a hosting panel with five capabilities: to install applications, to configure them, to update them, to do user management, and to switch customers to and from other hosting providers. This part connects each capability to the requirements in the specifications, to the code, to the tests and to the docs pages. It also shows where the code has no requirement.

How to read the tables

The specifications are in the directories core/specs, api/specs and panel/specs. A specification gives its requirements as numbered items, for example FR-001. The overview of the specifications gives the specifications of core.

A capability has more than one function. Each row of the tables is one function. The list of functions is made by hand in core/docs/md/technical-design/traceability.nix. The build of the docs stops when a row gives a path that is not in the repository, or a requirement that is not in the spec file. The build does not examine whether a test proves a requirement.

The build calculates the status of a row with these rules, in this sequence:

  1. no spec: the row has no requirement.
  2. planned: each requirement of the row has a marker (planned, #<number>) in its spec file. The status gives the issues.
  3. partial, planned: some requirements of the row have that marker.
  4. partial: the row has a gap, or the row has no test, or the status of a cited spec file is not Implemented. A spec file with the status Partially implemented does not say which of its requirements are open, thus each row that cites it is partial.
  5. built: all other rows.

A requirement that the code does not have gets the marker (planned, #<number>), where the number is the issue for the work. At this revision, no requirement in the three directories has the marker.

A spec file that has the section Known Deviations gives there the differences between the code and the design, and the issue of each difference when one exists. The table of the specification files gives these issues. The build does not read the state of an issue, thus an issue in the table can be closed.

Requirements, tests and status

Capability Function Spec and requirement Test Status
Install The catalog of the applications that an operator can select core/specs/003-applications-catalog/spec.md FR-001, FR-002, FR-003, FR-004, FR-005; api/specs/003-application-catalog/spec.md FR-001, FR-002, FR-003, FR-004 core/checks/applications-catalog.nix, api/src/fediversity_api/tests/test_http_application_list.py built
Install The operator submits a deployment, and a job builds and deploys it panel/specs/002-deployment-lifecycle/spec.md FR-001, FR-002; api/specs/001-deployments-and-builds/spec.md FR-001, FR-003, FR-011; api/specs/005-job-execution/spec.md FR-001, FR-002, FR-003, FR-004, FR-005; core/specs/004-deployment-programs/spec.md FR-001 panel/src/panel/tests/test_deployment_form.py, panel/checks/js-tests/deployment-submit.test.js, panel/checks/apps-panel, api/src/fediversity_api/tests/test_deployment.py, api/src/fediversity_api/tests/test_http_deployment.py, api/checks/apps-api, core/checks/deployment/tf-incus-operator built
Install The components of the applications Mastodon, PeerTube and Pixelfed core/specs/008-components/operator/mastodon.md FR-001, FR-002; core/specs/008-components/operator/peertube.md FR-001, FR-002; core/specs/008-components/operator/pixelfed.md FR-001, FR-002 core/checks/apps, core/components/operator/mastodon/test-script.py, core/components/operator/peertube/test-script.py, core/components/operator/pixelfed/test-script.py built
Install The domain of a deployment: the claim, the DNS records and the certificates api/specs/001-deployments-and-builds/spec.md FR-004; core/specs/009-deployment-methods/tf-incus-operator.md FR-018, FR-019; core/specs/007-resources/ssl.md FR-001, FR-003, FR-006; panel/specs/002-deployment-lifecycle/spec.md FR-003, FR-013, FR-015, FR-019 api/src/fediversity_api/tests/test_apex_claims.py, api/src/fediversity_api/tests/test_domains.py, panel/src/panel/tests/test_domain_page.py, core/checks/deployment/domain-claim-probe built
Install panel shows the addresses of the deployed instances api/specs/001-deployments-and-builds/spec.md FR-004, FR-006; panel/specs/004-instances-overview/spec.md FR-001, FR-003 api/src/fediversity_api/tests/test_instances.py, panel/src/panel/tests/test_instance_list.py built
Configure The form of the configuration comes from the options of the application modules core/specs/002-configuration-schema/spec.md FR-001, FR-003, FR-008, FR-010; api/specs/002-schema-and-sources/spec.md FR-001, FR-006, FR-007; panel/specs/003-configuration-form/spec.md FR-001, FR-002 core/lib/json-schema-test.nix, core/lib/application-schemas-test.nix, api/src/fediversity_api/tests/test_http_schema.py, panel/src/panel/tests/test_schema.py built
Configure A stored configuration follows the renames of options core/specs/002-configuration-schema/spec.md FR-005; api/specs/002-schema-and-sources/spec.md FR-008, FR-009, FR-010; panel/specs/003-configuration-form/spec.md FR-003 core/lib/renames-for-source-test.nix, api/src/fediversity_api/tests/test_migrate.py, panel/checks/apps-panel built
Configure The operator gives secrets that the configuration does not keep core/specs/002-configuration-schema/spec.md FR-011; panel/specs/003-configuration-form/spec.md FR-009 core/lib/operator-secrets-test.nix, api/src/fediversity_api/tests/test_operator_secrets.py, panel/src/panel/tests/test_deployment_secrets.py built
Configure A new deployment starts from the defaults that the modules declare core/specs/002-configuration-schema/spec.md FR-003; panel/specs/003-configuration-form/spec.md FR-007 api/src/fediversity_api/tests/test_config_schema.py, panel/src/panel/tests/test_deployment_form.py built
Update An update policy for each deployment api/specs/001-deployments-and-builds/spec.md FR-014, FR-015, FR-016; panel/specs/002-deployment-lifecycle/spec.md FR-009, FR-010, FR-011, FR-012 api/src/fediversity_api/tests/test_update_policy.py, panel/src/panel/tests/test_update_policy.py, panel/checks/js-tests/update-policy.test.js built
Update The source of the modules that a deployment builds from api/specs/002-schema-and-sources/spec.md FR-004, FR-005, FR-011 api/src/fediversity_api/tests/test_source_allowlist.py, api/src/fediversity_api/tests/test_self_pin_warm.py built
User management An operator account and its login in panel panel/specs/001-accounts-and-ownership/spec.md FR-001, FR-002 panel/src/panel/tests/test_login.py, panel/src/panel/tests/test_oidc.py built
User management A deployment has one owner, and api authorizes each call for that owner panel/specs/001-accounts-and-ownership/spec.md FR-003, FR-004, FR-006; panel/specs/005-api-authorization/spec.md FR-001, FR-002, FR-003; api/specs/004-authorization/spec.md FR-001, FR-002 panel/src/panel/tests/test_account_api_access.py, api/src/fediversity_api/tests/test_authz_callback.py, api/src/fediversity_api/tests/test_authz_required.py built
User management The removal of an operator account panel/specs/001-accounts-and-ownership/spec.md FR-007 panel/src/panel/tests/test_account_removal.py built
User management The hosting provider manages the operator accounts in the administration pages of panel -- -- no spec
User management A directory of the users and groups of a deployment core/specs/008-components/shared/lldap.md FR-001, FR-003, FR-006; core/specs/007-resources/ldap.md FR-002, FR-008, FR-009 core/checks/integrations/lldap-series, core/checks/integrations/lldap-provision, core/resources/ldap/provider-test.nix built
User management Single sign-on of the applications core/specs/008-components/shared/authelia.md FR-001, FR-006; core/specs/007-resources/oidc.md FR-006, FR-013; core/specs/008-components/operator/mastodon.md FR-004; core/specs/008-components/operator/peertube.md FR-004; core/specs/008-components/operator/pixelfed.md FR-006 core/checks/integrations/authelia-series, core/components/operator/mastodon/oidc-test.nix, core/resources/oidc/policy-test.nix built
User management The groups of the directory give the roles in an application -- core/resources/ldap/provider-test.nix, core/components/operator/mastodon/test-script.py, core/components/operator/peertube/test-script.py, core/components/operator/pixelfed/test-script.py no spec
Switch The backups of the databases and the media of the applications, and the operations that start a backup or a restore api/specs/001-deployments-and-builds/spec.md FR-018; core/specs/007-resources/garage.md FR-020, FR-022, FR-023, FR-027; core/specs/008-components/operator/mastodon.md FR-013; core/specs/008-components/operator/peertube.md FR-012; core/specs/008-components/operator/pixelfed.md FR-014 core/checks/integrations/media-backup, core/checks/integrations/app-database-restore, core/checks/deployment/app-operations, api/src/fediversity_api/tests/test_operations.py built
Switch The export of a deployment to a different hosting provider -- api/src/fediversity_api/tests/test_migration.py, api/checks/migration-kv-test.nix, core/checks/deployment/migration-quiesce, panel/checks/js-tests/migration-export.test.js no spec
Switch The import of a deployment from a different hosting provider, and the restore of its data core/specs/009-deployment-methods/tf-incus-operator.md FR-031, FR-032; panel/specs/002-deployment-lifecycle/spec.md FR-025; api/specs/004-authorization/spec.md FR-014 core/checks/integrations/migration-restore, api/src/fediversity_api/tests/test_migration_restore.py, api/src/fediversity_api/tests/test_import_check.py, panel/checks/js-tests/migration-import.test.js partial
Switch The transfer of a domain to and from the registrar of the hosting provider core/specs/010-effects/registrar-providers.md FR-011, FR-012, FR-028, FR-029, FR-031; core/specs/009-deployment-methods/tf-incus-operator.md FR-033; panel/specs/002-deployment-lifecycle/spec.md FR-020, FR-021 core/checks/deployment/registrar-transfer, api/src/fediversity_api/tests/test_domain_transfer_in.py, api/src/fediversity_api/tests/test_domain_take_out.py, api/src/fediversity_api/tests/test_domain_handover.py partial

Code and docs

A path that is a directory contains the code or the test.

Capability Function Code Docs
Install The catalog of the applications that an operator can select core/applications-catalog.nix, api/src/fediversity_api/catalog.py, api/src/fediversity_api/routes/applications.py Add an application
Install The operator submits a deployment, and a job builds and deploys it panel/src/panel/views.py, api/src/fediversity_api/routes/deployments.py, api/src/fediversity_api/deployment.py, api/src/fediversity_api/_runner.py Make a deployment
Install The components of the applications Mastodon, PeerTube and Pixelfed core/components/operator/mastodon, core/components/operator/peertube, core/components/operator/pixelfed Add an application
Install The domain of a deployment: the claim, the DNS records and the certificates api/src/fediversity_api/domain_claims.py, api/src/fediversity_api/apex_claims.py, api/src/fediversity_api/routes/domains.py, core/resources/dns, core/resources/ssl --
Install panel shows the addresses of the deployed instances api/src/fediversity_api/instances.py, panel/src/panel/views.py, panel/src/panel/templates/instance_list.html --
Configure The form of the configuration comes from the options of the application modules core/lib/json-schema.nix, core/lib/schema-for-source.nix, api/src/fediversity_api/schema.py, api/src/fediversity_api/routes/schema.py Operator configuration, Schema and migration sources
Configure A stored configuration follows the renames of options core/lib/renames-for-source.nix, api/src/fediversity_api/renames.py, api/src/fediversity_api/migrate.py Schema and migration sources
Configure The operator gives secrets that the configuration does not keep core/lib/operator-secrets.nix, api/src/fediversity_api/operator_secrets.py, panel/src/panel/secrets.py --
Configure A new deployment starts from the defaults that the modules declare core/lib/json-schema.nix, core/lib/schema-for-source.nix, panel/src/panel/views.py --
Update An update policy for each deployment api/src/fediversity_api/update_policy.py, api/src/fediversity_api/automatic_updates.py Update policy
Update The source of the modules that a deployment builds from api/src/fediversity_api/self_source.py, api/src/fediversity_api/source_allowlist.py Schema and migration sources
User management An operator account and its login in panel panel/src/panel/oidc.py Architecture of panel
User management A deployment has one owner, and api authorizes each call for that owner panel/src/panel/models.py, panel/src/panel/tokens.py, api/src/fediversity_api/authz.py, api/src/fediversity_api/callback.py Data model
User management The removal of an operator account panel/src/panel/models.py, panel/src/panel/admin.py --
User management The hosting provider manages the operator accounts in the administration pages of panel panel/src/panel/admin.py --
User management A directory of the users and groups of a deployment core/components/shared/lldap, core/resources/ldap/lldap-provider.nix, core/resources/ldap/provision.nix Manage the users of your deployment, Manage users and groups
User management Single sign-on of the applications core/components/shared/authelia, core/resources/oidc/authelia-oidc-provider.nix --
User management The groups of the directory give the roles in an application core/resources/ldap/application-role-groups.nix, core/resources/ldap/roles-consumer.nix, core/resources/ldap/publish-roles.nix --
Switch The backups of the databases and the media of the applications, and the operations that start a backup or a restore core/components/shared/postgres-backup.nix, core/components/shared/operations/backup.nix, api/src/fediversity_api/operations.py, core/resources/garage/media-backup.nix, core/resources/garage/media-backup.sh, core/resources/garage/restic-s3.nix --
Switch The export of a deployment to a different hosting provider api/src/fediversity_api/routes/migration.py, api/src/fediversity_api/migration.py, core/effects/migration/effect.nix, core/effects/migration/run.sh, core/components/shared/migration-quiesce.nix Export of a migration, Export a deployment for a migration, Switching and exit clauses
Switch The import of a deployment from a different hosting provider, and the restore of its data api/src/fediversity_api/routes/migration.py, api/src/fediversity_api/import_check.py, api/src/fediversity_api/migration_kv.py, core/effects/migration-restore/effect.nix, core/effects/migration-restore/run.sh Restore the apps of a migration
Switch The transfer of a domain to and from the registrar of the hosting provider core/effects/tf/registrars.nix, api/src/fediversity_api/registrar.py, api/src/fediversity_api/routes/domains.py --

Gaps

These functions are in the code, but the specifications do not fully cover them. A row with the status no spec has no numbered requirement at all.

Capability Function Gap
User management The hosting provider manages the operator accounts in the administration pages of panel No file in panel/specs has a requirement for the administration pages.
User management The groups of the directory give the roles in an application core/specs/007-resources/ldap.md has no requirement for the roles.
Switch The export of a deployment to a different hosting provider The design is prose in api/docs/design/migrations.md. No spec file has a numbered requirement for the export.
Switch The import of a deployment from a different hosting provider, and the restore of its data No file in api/specs has a numbered requirement for the import procedure.

Results of the grant

The grant agreement states the results of each capability in the section "Planned features", and two groups of milestones that help: "Front-end" and "Security". The text is in the amendment agreement/Amendment - AMD-101136078-2-1.pdf in the meta repository of the project. This table quotes each result and gives the functions of this page that cover it. The connection between a result and a function is made by hand in core/docs/md/technical-design/traceability.nix. The build stops when the table names a function that is not a row of this page.

no row means that no function of this page covers the result. It does not mean that the code does not have the result. The milestones are not capabilities, thus they have no row.

One function covers only a part of its result: panel shows the addresses of the applications on a page, and the code sends no message to the operator when a deploy is done.

Planned feature Result that the grant states Functions of this page
Install applications "Operators will be able to easily select desired applications to install (i.e. deploy), ideally in bulk." The catalog of the applications that an operator can select; The operator submits a deployment, and a job builds and deploys it
Install applications "Selected applications will be deployed to the operator's own domain." The domain of a deployment: the claim, the DNS records and the certificates
Install applications "The operator will be notified how to access their applications, once these are ready to use." panel shows the addresses of the deployed instances
Install applications "Operators indicate to be satisfied with the implemented solution." no row
User management "Operators have access to an administrator account on their deployed applications." The groups of the directory give the roles in an application
User management "Operators can create users accounts for their deployed applications from a central place, e.g. using LDAP." A directory of the users and groups of a deployment
User management "Log-in experience unified across applications using single sign-on (SSO)." Single sign-on of the applications
User management "Operators indicate to be satisfied with the implemented solution." no row
Switch hosting provider "Operators can back up their application data." The backups of the databases and the media of the applications, and the operations that start a backup or a restore
Switch hosting provider "Operators can trivially move deployments to a different hosting provider." The export of a deployment to a different hosting provider; The import of a deployment from a different hosting provider, and the restore of its data; The transfer of a domain to and from the registrar of the hosting provider
Switch hosting provider "Migrations gracefully handle Fediversity version differences." The import of a deployment from a different hosting provider, and the restore of its data; A stored configuration follows the renames of options
Update applications "Operators can set when to update their applications" An update policy for each deployment
Update applications "Operators are guided through new options on updates." no row (issue 3054)
Update applications "Updating deployment options automated." A stored configuration follows the renames of options
Configure applications "Configuration uses a batteries-included approach." A new deployment starts from the defaults that the modules declare
Configure applications "Relevant configuration options are available for operators to tweak, presented using a user-friendly web interface, that further aid the user in case updates change the available options." The form of the configuration comes from the options of the application modules; A stored configuration follows the renames of options
Front-end (milestone) "API specification published" no row
Front-end (milestone) "API available" no row
Front-end (milestone) "UX prototype finished" no row
Front-end (milestone) "web host application implements API" no row
Security (milestone) "Credentials handled" no row
Security (milestone) "TLS certificates handled" no row
Security (milestone) "Hypervisor users segregated" no row
Security (milestone) "Inter-node access authenticated" no row

The specification files

This table lists each file in the three directories that has a minimum of one numbered requirement. The column Planned markers counts the issues that the markers (planned, #<number>) of the requirements name. The column Issues of the known deviations gives the issues that the section Known Deviations of the file names. The last column gives the capabilities whose rows cite the file. A file with -- in that column covers a subject that is not one of the five capabilities, for example the data model or the rotation of secrets.

Spec file Title Status Requirements Planned markers Issues of the known deviations Capabilities that cite it
core/specs/001-data-model/spec.md Data model and extension seam Implemented 10 0 -- --
core/specs/002-configuration-schema/spec.md Configuration model and JSON Schema Implemented 13 0 issue 2132 Configure
core/specs/003-applications-catalog/spec.md Applications catalog Implemented 5 0 -- Install
core/specs/004-deployment-programs/spec.md Stateless deployment programs Implemented 9 0 -- Install
core/specs/005-setups-and-wiring/spec.md Setups and wiring Implemented 14 0 -- --
core/specs/006-testers-and-docs/spec.md Test harness, checks and documentation Implemented 9 0 -- --
core/specs/007-resources/dns.md Resource dns Implemented 8 0 -- --
core/specs/007-resources/firewall.md Resource firewall Implemented 11 0 -- --
core/specs/007-resources/garage.md Resource garage Implemented 38 0 -- Switch
core/specs/007-resources/generateFiles.md Resource generateFiles Implemented 12 0 -- --
core/specs/007-resources/ldap.md Resource ldap Implemented 18 0 -- User management
core/specs/007-resources/netbox.md Resource netbox Implemented 9 0 -- --
core/specs/007-resources/network.md Resource network Implemented 11 0 issue 2131 --
core/specs/007-resources/nixos-module.md Resource nixos-module Implemented 4 0 -- --
core/specs/007-resources/oidc.md Resource oidc Implemented 17 0 -- User management
core/specs/007-resources/otel.md Resource otel Implemented 6 0 -- --
core/specs/007-resources/pod-roles.md Resource pod-roles Implemented 15 0 -- --
core/specs/007-resources/postgresql.md Resource postgresql Implemented 19 0 -- --
core/specs/007-resources/redis.md Resource redis Implemented 11 0 -- --
core/specs/007-resources/secrets.md Resource secrets Implemented 9 0 -- --
core/specs/007-resources/smtp.md Resource smtp Implemented 14 0 -- --
core/specs/007-resources/spec.md Resource family Implemented 13 0 -- --
core/specs/007-resources/spiffe.md Resource spiffe Implemented 10 0 -- --
core/specs/007-resources/ssl.md Resource ssl Implemented 10 0 -- Install
core/specs/008-components/dev/forgejo-runner.md Component forgejo-runner Implemented 12 0 -- --
core/specs/008-components/dev/forgejo.md Component forgejo Implemented 17 0 issue 29 --
core/specs/008-components/dev/git-pages.md Component git-pages Implemented 8 0 issue 2126 --
core/specs/008-components/hosting-provider/bind.md Component bind Implemented 13 0 -- --
core/specs/008-components/hosting-provider/garage.md Component garage Implemented 4 0 -- --
core/specs/008-components/hosting-provider/incus.md Component incus Implemented 26 0 -- --
core/specs/008-components/hosting-provider/netbox.md Component netbox Implemented 12 0 -- --
core/specs/008-components/hosting-provider/openbao.md Component openbao Implemented 38 0 issue 2270 --
core/specs/008-components/hosting-provider/overview.md Component overview Implemented 3 0 -- --
core/specs/008-components/hosting-provider/postgresql.md Component postgresql Implemented 5 0 -- --
core/specs/008-components/hosting-provider/spire.md Component spire Implemented 20 0 -- --
core/specs/008-components/hosting-provider/telemetry.md Components telemetry and otel-collector Implemented 26 0 -- --
core/specs/008-components/hosting-provider/terraform-backend.md Component terraform-backend Implemented 15 0 -- --
core/specs/008-components/hosting-provider/windmill.md Component windmill Implemented 15 0 -- --
core/specs/008-components/operator/grafana.md Component grafana Implemented 7 0 -- --
core/specs/008-components/operator/mastodon.md Component mastodon Implemented 16 0 -- Install, User management, Switch
core/specs/008-components/operator/peertube.md Component peertube Implemented 13 0 -- Install, User management, Switch
core/specs/008-components/operator/pixelfed.md Component pixelfed Implemented 18 0 -- Install, User management, Switch
core/specs/008-components/shared/authelia.md Component authelia Implemented 12 0 -- User management
core/specs/008-components/shared/lldap.md Component lldap Implemented 10 0 -- User management
core/specs/008-components/shared/smtp.md Component smtp Implemented 4 0 -- --
core/specs/008-components/shared/valkey.md Component valkey Implemented 9 0 -- --
core/specs/008-components/spec.md Components Implemented 21 0 -- --
core/specs/009-deployment-methods/k8s.md Deployment method k8s Implemented 37 0 -- --
core/specs/009-deployment-methods/spec.md Method family deployment-methods Implemented 26 0 -- --
core/specs/009-deployment-methods/ssh.md Deployment method ssh Implemented 17 0 -- --
core/specs/009-deployment-methods/tf-incus-operator.md Deployment method tf-incus-operator Implemented 37 0 -- Install, Switch
core/specs/009-deployment-methods/tf-incus.md Deployment method tf-incus Implemented 23 0 -- --
core/specs/009-deployment-methods/tf.md Deployment method tf Implemented 11 0 issue 2220 --
core/specs/010-effects/image.md Effect tf-incus-image Implemented 15 0 issue 2131 --
core/specs/010-effects/incus-bootstrap.md Effect dev-tf-incus-bootstrap Implemented 9 0 -- --
core/specs/010-effects/netbox-seed.md Effect netbox-seed Implemented 12 0 -- --
core/specs/010-effects/octodns.md Effect octodns-zone Implemented 16 0 -- --
core/specs/010-effects/registrar-providers.md Registrar providers of the operator path Partially implemented 38 0 issue 1908 Switch
core/specs/010-effects/registrars.md Effects registrar-* Implemented 15 0 issue 2131 --
core/specs/010-effects/spec.md Effect family effects Implemented 14 0 issue 2129 --
core/specs/011-secret-rotation/spec.md Secret rotation Partially implemented 12 0 -- --
core/specs/012-license-report/spec.md License report Implemented, with the decisions in License decisions (core/docs/md/compliance/license-decisions.md) 11 0 -- --
api/specs/001-deployments-and-builds/spec.md Deployments and builds Implemented 19 0 -- Install, Update, Switch
api/specs/002-schema-and-sources/spec.md Schema, migrations and module sources Implemented 11 0 -- Configure, Update
api/specs/003-application-catalog/spec.md Application catalog Implemented 4 0 -- Install
api/specs/004-authorization/spec.md Authorization of callers Implemented 14 0 -- User management, Switch
api/specs/005-job-execution/spec.md Execution of deployment jobs Implemented 5 0 -- Install
api/specs/006-service-deployment/spec.md Deployment of the api service Implemented 5 0 issue 1940 --
panel/specs/001-accounts-and-ownership/spec.md Accounts and ownership Implemented 8 0 -- User management
panel/specs/002-deployment-lifecycle/spec.md Deployment lifecycle Implemented 26 0 -- Install, Update, Switch
panel/specs/003-configuration-form/spec.md Configuration form Implemented 9 0 -- Configure
panel/specs/004-instances-overview/spec.md Instances overview Implemented 3 0 -- Install
panel/specs/005-api-authorization/spec.md Authorization of calls to api Implemented 7 0 -- User management
panel/specs/006-service-deployment/spec.md Service deployment Implemented 7 0 -- --