Threat model
This page tells what Fediversity must protect, from whom, and how the code protects it. For each threat, it gives the control in the code, or the issue that tracks the gap. The page uses STRIDE to sort the threats: spoofing (S), tampering (T), repudiation (R), information disclosure (I), denial of service (D) and elevation of privilege (E).
The page states the current state of the code on main. When you change a
specification or a control on this page, update the page in the same pull
request. To report a vulnerability, read SECURITY.md.
Scope
The scope is the scope of SECURITY.md:
- the code in this repository, and the configuration that it gives to the applications that it deploys;
- the development infrastructure that this repository deploys, for example
git.fediversity.euand its CI runners.
The applications themselves, for example Mastodon or PeerTube, are out of scope. Their configuration is in scope.
Assets
| Asset | Where it is | Why it is important |
|---|---|---|
| Secrets in OpenBao | The KV, PKI, LDAP and database engines of the OpenBao node of each hosting provider (core/components/hosting-provider/openbao/) |
They give access to all services of all deployments. |
| The OpenBao unseal key and root token | The unseal key is sealed to the TPM of the OpenBao node. A copy of it, encrypted to the age recipients of fediversity.bootstrapBackupRecipients, is in /var/lib/openbao-init/unseal-key.age and in the offsite backup. No root token is on the disk: openbao-init and the admin command openbao-root-token make one with generate-root and revoke it at their end (openbao.nix). |
Each one gives full control of OpenBao. |
| Deploy credentials | The SSH key of CD (core/keys/cd-ssh-key.pub is its public half), the Forgejo Actions secrets, the OIDC token of a CD run, and the operator deploy credentials (core/components/hosting-provider/deploy-credentials.nix) |
A deploy runs as root on the nodes. |
| Contributor keys | core/keys/contributors/ |
They give root on the development infrastructure, and they decrypt the agenix secrets (core/keys/README.md) and the keys in the bootstrap backups (core/components/hosting-provider/backup-recipients.nix). |
| agenix secrets | core/resources/secrets/*.age, recipients in core/resources/secrets/secrets.nix |
Forge database and mail passwords, the runner registration token, the OpenPGP key for vulnerability reports. |
| The OpenTofu state | The terraform-backend node of the hosting provider (core/components/hosting-provider/terraform-backend/) |
The state maps the infrastructure and can contain secrets. |
| Migration strings | With the operator and the target hosting provider, from the export to the import. The target keeps the pointers in the OpenBao KV of the deployment, at vars/operator-deployments/<deployment>/secret/migration/pointers, until the restore (api/src/fediversity_api/migration_kv.py). |
A string gives the S3 credentials and the password of each snapshot repository, the keys that decrypt the data of the apps, and the auth code of the domain when the registrar account holds the domain. Thus it gives all user data of the deployment. |
| User data | The databases (core/resources/postgresql/), buckets (core/resources/garage/) and caches (core/components/shared/valkey/) of the applications, and their offsite backups |
Personal data of the application users of the applications (GDPR). |
| The Nix store and the binary caches | Each node, the nix-serve cache on the incus host and its signing key in /var/lib/incus-bridge-cache/ (core/components/hosting-provider/incus.nix), the stores of the CI runners |
Each node runs what its store contains. |
| The source code and its history | git.fediversity.eu (core/components/dev/forgejo/) and, when a deployment has one, the S3 store of its offsite backup (core/components/dev/forgejo/backup.nix) |
CD deploys what is on main. |
| The SPIRE trust bundle and the node identities | The SPIRE server of the hosting provider (core/components/hosting-provider/spire/). The private key of the SPIRE root of a deployment, when it has one, on the SPIRE node and in the Forgejo Actions secret SPIRE_ROOT_KEY |
A node logs in to OpenBao with its SVID. A copy of the CA key or of the root key signs an SVID for each host. |
Parties
- Hosting provider. It runs the incus host, the shared services (OpenBao,
SPIRE, the state backend, PostgreSQL, Garage, Authelia, lldap, telemetry,
windmill) and the panel and API. Its administrators are
fediversity-adminsin lldap, who log in to the sharedadminaccount of a node with a certificate. Theemergencyaccount of a node accepts the static offline keys inADMIN_EMERGENCY, for when no certificate is available. - Operator. A customer of the hosting provider. It configures and deploys applications through the panel. It does not get shell access to the nodes.
- Application User. A person who uses an application of an operator, for example a Mastodon account. Application Users and operators are on the internet.
- Contributor. A person with write access to the repository. A contributor
whose key is in
core/keys/contributors/is also an administrator of the development infrastructure. - CI and CD runners. Hosts that run Forgejo Actions jobs
(
core/components/dev/forgejo-runner/). The same pool runs the checks of pull requests and the CD jobs that deploy. - The internet. All other parties, which includes the persons who open an
account on
git.fediversity.euand send pull requests from a fork. - Upstream projects. nixpkgs, the pinned sources in
npins/, and the public binary caches. - External backup target. The provider of the S3 service that holds the copy of the backups, if a group sets one. It gets encrypted objects only.
Trust boundaries
- Internet to the public services. The ports that each node publishes to
everyone: HTTPS of the applications and the admin UIs, ACME on
:80, DNS on:53, and SMTP on:25on a node that takes DMARC reports. - Internet to the administrative surface. SSH on
:22and the incus API on:8443. The packet filter admits onlyADMIN_SOURCESto these ports (notes/private-access.md). - Operator to the hosting provider. The panel and the API, and the code that a deploy evaluates for an operator.
- Operator deployment to the hosting-provider mesh. The nodes of an operator deployment and the nodes of the hosting provider share one incus bridge.
- Node to the secret store. Each node gets only the secrets of its own role from OpenBao.
- Forge to CI and CD. A pull request runs code on the runners. A push to
maindeploys. - Contributors and administrators to the nodes. The SSH certificates that OpenBao signs, the deploy key of CD, the emergency keys, and the agenix keys.
- Upstream and caches to the Nix store. The store paths that a node gets from a substituter.
- Garage node to the external backup target. When a group sets a target, the garage node writes the encrypted backups to an S3 service of a different provider. See the rows of the backups in Operator deployment to the hosting-provider mesh.
Entry points
| Entry point | Who can reach it | Control | Code |
|---|---|---|---|
| HTTPS of applications and UIs | Everyone | TLS from the ssl resource. Authelia forward-auth on the admin UIs. |
core/resources/ssl/, core/components/shared/authelia/ |
:80 (ACME HTTP-01) |
Everyone, only on a node that gets a public certificate | Published only for the ACME challenge | core/setups/wiring/firewall-policy.nix |
DNS :53 |
Everyone, only on a public named |
DNSSEC signing, update-policy for dynamic updates |
core/components/hosting-provider/bind.nix |
SSH :22 |
ADMIN_SOURCES and the internal sources |
Packet filter, keys only, no passwords | core/resources/firewall/, core/setups/nixos/networking.nix |
incus API :8443 |
ADMIN_SOURCES and the internal sources |
Packet filter, client certificates, OIDC | core/setups/wiring/firewall-policy.nix, incus specification |
| Panel | Everyone | Django login or OIDC, CSRF origin check. The language form (/i18n/setlang/) needs no login, takes a CSRF token and changes only a cookie of that browser |
panel/src/panel/settings.py, panel/nix/component/panel.nix |
| Provider data | Everyone | /provider/ of the panel is read-only, takes no input, and gives only what the hosting provider publishes |
panel/src/panel/views.py |
| API | The panel, and browsers with a panel token | Authorization callback to the panel for each call | api/src/fediversity_api/authz.py, API authorization specification |
| OpenBao | Nodes of the mesh, administrators | mTLS listener, cert, jwt and oidc auth, one policy for each role |
core/components/hosting-provider/openbao/ |
| Metrics read path | The nodes of operator deployments, and everyone that reaches telemetry.<domain> |
A credential for each deployment, a rate limit for each deployment, read routes only, and a deployment_id that prom-label-proxy adds to each query |
|
| State backend | The internal sources | Packet filter, HTTPS through nginx, a password for each deployer | core/components/hosting-provider/terraform-backend/ |
git.fediversity.eu |
Everyone | Anubis bot gate, Forgejo accounts | core/components/dev/forgejo/ |
| Forgejo Actions | Repositories of the fediversity organization, and pull requests to them |
One job for each runner registration, no secrets for fork pull requests | core/components/dev/forgejo-runner/forgejo-runner.nix |
Threats and controls
Internet to the public services
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| S, I | An attacker reads or changes traffic to a service. | Each served name gets a certificate from the ssl resource: ACME or the private CA in OpenBao (core/resources/ssl/, ssl specification). The panel sets forceSSL in production (panel/nix/configuration.nix). |
Traffic between services on the bridge is not always encrypted (#2179). |
| S | An attacker guesses the password of an admin UI. | Authelia regulation locks out after failed logins (core/components/shared/authelia/default.nix). |
The admin UIs need only one factor (#2181). |
| E | An attacker uses a known vulnerability in a deployed package. | A daily vulnix scan of the closure of each node fails on each CVE that is not in the triage list (.forgejo/workflows/vulnerability-scan.yaml). The daily run opens an issue for each new CVE of high or critical severity (.forgejo/workflows/vulnix-report.py). Each node has a CycloneDX SBOM of its runtime closure (core/setups/nixos/sbom.nix, system.build.sbom). The deploy of a node makes its SBOM, and the API gives it (GET /sbom/{deployment_id}). CI publishes the SBOM of each node of main (SBOM). |
Known CVEs wait for a fix (#2177, #2208). |
| T | A page of another site reads the media of an instance with a script. | The CORS rule of the media buckets of Mastodon and Pixelfed allows GET only from the origin of the application (mkGarageRequest in core/components/operator/app-resources.nix, Mastodon and Pixelfed specifications). |
The media buckets of PeerTube allow GET from any origin, for the players of other instances (PeerTube specification). The buckets need no credentials, thus a script reads only what a direct request reads too. |
| D | An attacker floods a service. | Anubis on the forge (core/components/dev/forgejo/anubis.nix), the ICMP rate limit of the node (core/setups/nixos/networking.nix). |
The nginx configuration of the services has no rate limit, except for the password login of the panel. |
Internet to the administrative surface
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| S, E | An attacker logs in with SSH. | The input-gate chain admits :22 and :8443 only from adminSources and the internal sources (core/resources/firewall/nixos.nix). A node with a public address publishes no administrative port (core/setups/wiring/firewall-policy.nix). SSH takes keys only, PasswordAuthentication = false (core/setups/nixos/networking.nix). With ADMIN_SSH_CA_KEYS, an account also accepts a certificate for its own name, which OpenBao signs after an OIDC login with a second factor. The certificate expires after 8 hours, and root accepts no certificate (core/components/hosting-provider/openbao/ssh-certificates.nix). |
When ADMIN_SOURCES is empty, the administrative ports are open to all sources. The design permits this only on the management network (firewall-policy.nix). root can log in with SSH (#24, #295). |
| S | A deploy connects to a false host. | None. | The ssh method turns off the host-key check (#2137). |
| E | A user with OIDC access to incus changes all instances. | The incus OIDC login goes through Authelia, and Authelia refuses a user who is not in fediversity-admins (core/components/hosting-provider/incus/default.nix). The authorization scriptlet of incus also refuses each request of an OIDC user without fediversity-admins in the groups claim (core/components/hosting-provider/incus/oidc.nix). |
The scriptlet has no finer rights: each member of fediversity-admins is an administrator of all instances (incus specification). |
Operator to the hosting provider
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| S | An attacker guesses the password of an operator on the panel. | The panel checks the strength of a new password (panel/src/panel/settings.py, AUTH_PASSWORD_VALIDATORS). With OIDC, the login goes through Authelia. nginx limits the POST requests to /login/ and /admin/login/ to 5 a minute for each client address (panel/nix/configuration.nix, panel_login). |
|
| S, I | An attacker on the network takes the session cookie of an operator. | In production, nginx sends HTTP requests to HTTPS and sends an HSTS header, and the session and CSRF cookies have the Secure flag (panel/nix/configuration.nix). |
|
| S, E | An operator acts on a deployment of a different operator. | The API asks the panel for each call (api/src/fediversity_api/authz.py). The panel allows a call only for the deployments of the requester, and for a list without ids it gives the ids of the deployments of the requester, which api/ limits the list to (panel/src/panel/views.py, AuthzCallback). The callback checks the bearer token of the API with hmac.compare_digest. The panel component sets the token (panel/nix/component/panel.nix). Without the token, the callback takes requests from all callers and writes a warning. |
|
| E | An operator makes a deploy run code that the hosting provider did not accept. | The API accepts a module_source only from an allowlist of exact repository URLs, and only a revision on a branch (api/src/fediversity_api/source_allowlist.py). The default allowlist holds only this repository (api/nix/component/default.nix, trustedSourceUrls). |
|
| I | An operator secret goes into the Nix store. | Deploy credentials go to the deploy as environment variables, not into the configuration file. The API removes them from the windmill job (core/components/hosting-provider/deploy-credentials.nix). The form marks a secret that the operator brings writeOnly. The API writes it to the OpenBao KV of the deployment, and the node reads it at run time (core/lib/operator-secrets.nix). The evaluation fails when such a value is in the settings. |
A configuration from before this rule keeps the value until the next submit, and its stored builds keep it. When the API does not give the schema, the panel stores its draft as it is. |
| I | A secret is the same for all deployments. | None. | Fixed restic password and a PeerTube placeholder secret (#2136). |
| I | A migration string leaks. | An export needs the authorization of the deployment, and the string goes only to the caller. The api keeps only the snapshot references in the deployment row, and the audit event holds only the result (api/src/fediversity_api/migration.py). The target writes the pointers only to the KV, and each node gets its pointer on stdin (core/effects/migration-restore/run.sh). The deploy deletes the KV entry after the restore. The api deletes the windmill job of an export when the job completes (api/src/fediversity_api/node_effect.py). |
The string has no end time. The source and the target share each key that the string carries, until an operator changes the key, and some keys cannot change without data loss. The windmill database holds the pointers of an export while the job runs, and after the job if the delete fails: the api logs the job id, and an administrator must delete the job. upload-vars-openbao gives each value to bao kv put as an argument, thus the process table of the node shows the value during the upload. This is the same for all vars. |
| I | An operator reads the metrics of a different operator. | Each operator has a Grafana node, and its only datasource is the read path of the deployment. The credential of the read path is for one deployment, and prom-label-proxy adds its deployment_id to each query (core/components/hosting-provider/metrics-query.nix). |
|
| D | A deployment slows down the read path of the others with costly queries or checks. | The rate limit is for each deployment, and comes before the password check. The telemetry node accepts only a bcrypt hash of cost 5 from KV. A query can use the CPU and memory of the shared Mimir, in the limits of the rate. | |
| I | A client finds out which deployments have a read path. | None. The read path gives 403 for a deployment without a credential, and 401 for a wrong credential. The deployment ids are in public DNS names. | |
| I | A login on the Grafana node of a deployment shows its metrics to a user of that deployment. | Only the members of fediversity-admins of the deployment can log in, and each one is a Viewer. |
|
| R | Nobody knows who started an action. | The api records an audit event with the actor for each action that changes a deployment, in the audit_event table and in the journal (api/src/fediversity_api/audit.py). The audit shipper of the node sends the journal lines to the tenant audit of Loki, which refuses deletion, on a path of the ingest vhost that only the credential of the api opens (core/components/hosting-provider/audit-shipper.nix). The audit-ingest collector writes the source of that credential to each line, thus a node cannot write a line with the source of another node (audit-ingest.nix). Each event holds the hash of the event before it, and services.fediversity-api.auditHead.publishCommand sends the head of that chain off the fleet, thus a changed or removed event shows. |
#2182. |
Operator deployment to the hosting-provider mesh
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| S | A node uses the address of a different node. | incus MAC, IPv4 and IPv6 filtering on each NIC (core/effects/tf/incus/instance.tf). |
|
| I, T | A node of one operator reaches a node of a different operator. | Each operator deployment gets its own /28, and an incus network ACL drops traffic to the rest of the operator prefix (core/effects/tf/incus/operator/bridge-isolation.tf, core/components/hosting-provider/incus-bridge-acls.nix). The firewall gate of each operator node trusts only the provider prefix of the bridge and the bridge addresses of its own deployment, thus it also drops the other operator nodes on its unpublished ports (internalSources in core/setups/hosts-common.nix). Without NetBox, the deploy stops. incus drops all IPv6 on the bridge, because the bridge has no IPv6 subnet. |
|
| I, E | A node of an operator reaches the services of the hosting provider. | The bridge ACL and the firewall gate of each node of the hosting provider let an operator node reach only the ports of the resources that the node supplies to it (core/components/hosting-provider/operator-reach.nix, core/effects/tf/incus/bridge-provider-acl/, operatorSources in core/setups/hosts-common.nix). The addresses of a cluster (CLUSTER_SOURCES) get the same gate rule, but they do not go through the bridge ACL. A unit test reads the dials of the operator nodes and fails on a port that is not in the list (core/setups/operator-reach-test.nix). Each service asks for a credential: an SVID for OpenBao, scram-sha-256 over TLS for PostgreSQL (core/resources/postgresql/postgresql-provider.nix), passwords and ACLs for valkey (core/components/shared/valkey/valkey.nix). |
LDAP is plain ldap:// (#2139), and its consumers bind as the lldap admin (#2140). The ACL limits each public IPv4 address of the hosting provider to the operator ports and the published ports of the node. Only the gate limits the public IPv6 addresses (#2827). |
| I, E | A node on the container bridge reaches the services of the hypervisor. | The firewall gate of the hypervisor accepts from incusbr0 only the ports that the nodes of the hosting provider dial (core/components/hosting-provider/hypervisor-reach.nix, bridgeInterfaces in core/setups/wiring/firewall-policy.nix), and from the operator prefix only the ports of instances. A unit test reads the dials of the nodes of the hosting provider and fails on a port that is not in the list (core/setups/hypervisor-reach-test.nix). The incus API (:8443) asks for a trusted client certificate, and ssh (:22) for a key. |
|
| T, I | A node reads or changes the OpenTofu state. | The state is encrypted at rest with a key that the node makes (core/components/hosting-provider/terraform-backend/terraform-backend.nix). The backend binds loopback only. Its nginx front is reachable only from the internal sources, and it accepts a request for a state only from a deployer of that state, each with its own password (core/components/hosting-provider/terraform-backend/front.nix). |
A process on the node reaches the backend on loopback without a credential. cd and each workstation-<person> have the same permissions. operator can read only the addresses document of the hosting provider, not its full state with the secrets. After a new deploy of the hosting provider, an operator deploy fails until the first http apply of the hosting provider writes that document. The lock is in memory (terraform-backend specification). |
| D | A deployment loses the objects of its media buckets. | A timer on the application node copies the media buckets each day into the backups bucket of the application, with a history of the replaced and deleted objects (core/resources/garage/media-backup.nix). A VM test does a restore (core/checks/integrations/media-backup/). |
The operator can turn the backup off. It is in the same garage as the source (#2101). |
| I | A party that reads the backups bucket reads the media. | rclone crypt encrypts the name and the content of each object with a password of the deployment from OpenBao. The backup key may read the media buckets, and may not write them. |
The metadata of an object, for example its content type, is not encrypted. |
| I | A backup of an application keeps a deleted or erased record or object. | All the backups of an application keep its data for 30 days (backupRetention.application in core/components/operator/app-resources.nix): restic prunes with --keep-within 30d, and the history of the media backup expires after retentionDays, which has the same default. media-backup-<app> erase removes an object from the media backup and from all of its history. After a restore of the media alone, the operator runs the cleanup of orphan media of the application. |
The operator runs erase and the cleanup. The database backups have no erase command, thus an erased record stays in them for 30 days. The database backups of lldap, Authelia, the panel and api/ keep the accounts of the hosting provider, the configuration of the deployments and the audit_event table for 30 days too. The other backups of the hosting provider keep their data for approximately 6 months, for example the users of Grafana and NetBox. After a restore from a backup that is older than an erasure, the operator does the erasure again before the service opens to its users. The operation "Record an erasure" keeps the time and the reference of each erasure on the application node, and a restore from an older snapshot lists them until the operator confirms them. The restore does not do them again itself, and a new node starts with no record (hosting-provider/backups.md, "Erase personal data"). |
| D | The site loses the garage node, and with it all the backups. | A group can set backups.externalCopy: a daily timer on the garage node copies each *-backups bucket to an S3 target outside the site (core/resources/garage/external-copy.nix). A person keeps the key of the target outside the site, thus a restore does not depend on OpenBao. No group sets a target by default, and a group with no target stays open to this threat (#2101). |
|
| I | The external backup target reads the backups. | The copy holds the objects as they are at the source: restic and rclone crypt encrypt them, with keys that the target does not have. The target sees the names of the buckets, the object sizes and the times, and the hashed names of a restic repository. It is a sub-processor. |
|
| T, D | An attacker with root on the garage node, or a fault of the node, removes the copy. | The garage node holds a key that can delete in the target, thus the copy is not protected from that node. history/ keeps a replaced or removed object for historyDays. The unit stops when most of the copied buckets are empty in the source. A person can turn on versioning or object lock at the provider of the target. The code does not do this, and this is an open gap (#2101). |
|
| I | An application node reads the key of the target. | Only the garage node has a grant on vars/secret/backup-external-copy (externalCopyHosts in core/components/hosting-provider/openbao/spire-auth.nix). The key goes only to the unit of the copy. A run mints its own read key at the garage admin API, and deletes it at the end. |
Node to the secret store
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| S | A node reads the secrets of a different node. | A node logs in to OpenBao with its SPIFFE SVID (cert auth). Each roster host has its own role and policy (core/components/hosting-provider/openbao/spire-auth.nix). The deploy gives each agent the SPIRE trust bundle over incus exec and ssh (core/effects/tf/incus/host/register-ek.sh). OpenBao gets the bundle from the Workload API of its own agent (core/components/hosting-provider/openbao/spire-ca-bundle.sh). With a SPIRE root, each agent also has the root in its config at eval time, and the server signs each CA with it (core/components/hosting-provider/spire/spire.nix). |
|
| S | An attacker with a copy of the SPIRE CA key or root key logs in as each node. | The CA key is on the SPIRE node only, and the CA lives for about one day. The root key is on the SPIRE node with mode 0600, outside the state of the server and of each backup. The deploy writes it over the access of register-ek.sh and does not print it (core/effects/tf/incus/host/push-spire-root-key.sh). A rotation with an overlap removes a root (core/specs/008-components/hosting-provider/spire.md). |
The root signs each CA directly, with no offline root, thus a copy of the root key is valid until a rotation removes the root from each node, or for the lifetime of the root (10 years). The forge secret and each runner of a job of the hosting provider hold the root key. |
| I | An attacker reads the unseal key or the root token from the disk. | The unseal key is sealed to the TPM, with no plaintext fallback. The copy on the disk is encrypted to age recipients (openbao.nix). |
The scoped token of each unit is in a file, and the reconciler state holds secrets (OpenBao specification). The data on the hypervisor disks is not encrypted (#2180). |
| D | OpenBao or SPIRE loses its state. | The offsite backup holds a raft snapshot and the unseal key, encrypted to age recipients, thus it restores on a new node (openbao/snapshot.nix). |
Without a SPIRE root, a recovery after the SPIRE server loses its state gives a new bundle to each agent and to OpenBao (#2215). No replication (#2187). |
| E | A process on the OpenBao node makes a root token without a token. | generate-root needs the unseal key. Only the loopback listener 127.0.0.1:8252, which nginx does not proxy, serves it without authentication (openbao.nix). |
A process on the node can cancel an attempt, thus stop a restore on a new node. |
| R | Nobody knows who read a secret. | OpenBao writes each request and each response to the journal of its node as JSON, with secret values as HMAC-SHA256 hashes (openbao.nix). The journal goes to Loki (hosting provider guide). |
The audit log is not tamper-evident (#2270). The audit shipper and the tenant audit can take it, through wiring.auditLog. |
Forge to CI and CD
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| E | A pull request from a fork gets the deploy credentials. | Forgejo holds the workflows of a fork pull request until a maintainer approves them, unless the author has write access or a maintainer chose "always trust" for that user (Forgejo services/actions/trust.go). No workflow uses pull_request_target or issue_comment. A fork pull_request run gets no secrets and a read-only token. Only the organization registration token is used, so a repository of a different user cannot use the runners (core/components/dev/forgejo-runner/forgejo-runner.nix). |
After one approval, the job runs on the host (:host labels), and each account with write access runs code there with no review. The runner of a job has a UID of its own, registers for that one job and loses all state after it, so a job cannot change the environment of a later CD job or get its task. Jobs share the network namespace of the host, the disk and the Nix daemon as resources. An account with write access can also read secrets from a workflow on a branch, or start CD on the head of a pull request (#2228). |
| E | A run that is not on main deploys production. |
The OpenBao jwt role accepts only a token of this repository on refs/heads/main. The token lives for 30 minutes (core/components/hosting-provider/openbao/cd-oidc.nix). |
CD and pull requests use the same runner hosts and the same Nix daemon. The graphs take the Incus project as an input, and the hypervisor declares it (core/effects/common/incus-project.nix). Its value is default, thus CD has no Incus project of its own (#1638). |
| T | A CI build goes into a public cache. | #92. | |
| T | The code that CD deploys is not reviewed. | The branch protection rule on main has required_approvals: 0 and enable_push: true in the forge settings. Thus a user with write access can change main with no review (#291, #1487). |
|
| D | The forge loses its data. | No backups of Forgejo (#29). |
Contributors and administrators to the nodes
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| E | A lost contributor key gives root on all nodes. |
The keys are in the repository (core/keys/), so the history shows each change. |
Each contributor key is a recipient of the agenix secrets of each host (core/resources/secrets/secrets.nix, secrets specification). Administrators get sudo with no password and are trusted Nix users (core/setups/nixos/users.nix). |
| R | Nobody knows which administrator did an action. | The journal of each node goes to Loki (maintainer guide). The logs keep data for a limited time (hosting provider guide). | The journal keeps the full client IP address. |
Upstream and caches to the Nix store
| STRIDE | Threat | Control | Gap |
|---|---|---|---|
| T | A changed source goes into a build. | All sources are pinned with a hash in npins/sources.json. |
|
| T | A node takes a false store path from a cache. | The bridge cache listens only on the bridge, and the NIC filters stop address spoofing on the bridge. The host signs each path that the bridge cache serves, and a node accepts a path only with a signature from its host or from an upstream cache (require-sigs, core/setups/nixos/incus-bridge-cache.nix). The host changes its signing key every 90 days, and the nodes trust the previous key for 30 more days. After a compromise, the operator changes the key and stops the trust in the old key at once (incus-bridge-cache-key-revoke, hosting provider guide). |
An instance that does not have the key of its host, for example a container on a host that has no key yet or a VM without the incus agent, accepts unsigned paths in the bridge step of core/effects/incus/run.sh (--no-require-sigs) (#2277). |
Accepted risks and open gaps
These issues track the gaps on this page:
- Access: #24, #295, #1208, #2137, #2181.
- Secrets: #2136, #2140, #2215.
- Encryption: #2139, #2179, #2180.
- Audit and logs: #2182.
- CI and CD: #92, #291, #1487, #1638, #2228, #2277.
- Data and availability: #29, #2097, #2101, #2186, #2187.
- Vulnerability handling: #2176, #2177, #2208.
The Known Deviations sections of the specifications give more gaps, for each component.
Maintenance
CONTRIBUTING.md tells when a pull request updates this page. The
specifications are the reference for each control. This
page links to them, and does not repeat them.