Files
dendritic/.github/instructions/soteria-host.instructions.md
2026-07-05 00:03:28 -05:00

9.7 KiB

description, name, applyTo
description name applyTo
Use when modifying the Soteria host definition or host-local secrets under modules/hosts/soteria. Covers host wiring and SSH cert mechanics specific to Soteria. Soteria Host Instructions modules/hosts/soteria/**/*.nix, modules/hosts/soteria/secrets.yaml

Soteria Host Instructions

Use this instruction when changing Soteria host wiring in modules/hosts/soteria.

Host Intent And Shape

  • Treat Soteria as a NixOS service node for Forgejo, RESTic server, mTLS, and a paired Home Manager profile for john.
  • Keep host identity aligned: hostname = "soteria", networking.hostName = hostname, and flake.nixosConfigurations."${hostname}".
  • Keep host-local secrets in modules/hosts/soteria/secrets.yaml and wire with sops.defaultSopsFile = ./secrets.yaml.
  • Keep host-specific Home Manager wiring in flake.modules.homeManager.soteria, and keep mysops.hostSecretFile pointing at the Soteria secrets file.

Required Module Composition

When editing modules/hosts/soteria/default.nix, preserve this composition unless the task explicitly changes architecture:

  • Import nixos.ssh-certs to enable host SSH cert integration.
  • Import nixos.janus-ca so Step client trust material and defaults remain available.
  • Import nixos.restic-server so the RESTic REST server implementation lives in the shared module rather than inline host wiring.
  • Keep ssh-certs.hostname = hostname; so cert principals match host naming.

SSH Cert Mechanics On Soteria

Soteria SSH certificate behavior is the result of multiple modules. Keep this flow intact:

  1. soteria/default.nix imports nixos.ssh-certs and sets ssh-certs.hostname = "soteria" (via hostname).
  2. modules/services/step-ca/ssh-host.nix:
    • Enables ssh.certificates.enable = true.
    • Requires sops.secrets."janus/admin_jwk" (provisioner credential) from Soteria's secrets.yaml.
    • Defines cert paths at /etc/ssh/ssh_host_ed25519_key and /etc/ssh/ssh_host_ed25519_key-cert.pub.
    • Exposes ssh-host-cert-renew and ssh-host-cert-check helper binaries.
    • Schedules ssh-certs-renew.timer every 4h with jitter.
  3. modules/services/ssh.nix consumes ssh.certificates.enable and configures OpenSSH to:
    • Set TrustedUserCAKeys = /etc/ssh/ssh_user_ca_key.pub.
    • Set HostCertificate = /etc/ssh/ssh_host_ed25519_key-cert.pub.
    • Install the trusted user CA file into /etc/ssh/ssh_user_ca_key.pub.
  4. nixos.janus-ca provides Step CA trust bootstrapping (/etc/step-ca/defaults.json and linked root CA material), allowing Step CLI operations to trust and reach the CA endpoint.

Current implementation note:

  • ssh-certs-renew.service currently checks renewal state via step ssh needs-renewal and logs status. It does not directly invoke ssh-host-cert-renew in the service script. Preserve this behavior unless the task explicitly asks to change renewal execution semantics.

SOPS/Secrets Mechanics On Soteria

Soteria secret handling spans host-local secrets, NixOS secret materialization, and Home Manager secret tooling. Keep this flow intact:

  1. modules/hosts/soteria/default.nix imports nixos.mysops and sets sops.defaultSopsFile = ./secrets.yaml.
  2. The SOPS file modules/hosts/soteria/secrets.yaml is the canonical encrypted source for this host's system secrets.
  3. Shared modules imported by Soteria declare required secret entries under sops.secrets and consume them through config.sops.secrets.<name>.path:
    • modules/services/step-ca/ssh-host.nix declares and consumes janus/admin_jwk as the Step provisioner password file.
    • modules/features/forgejo.nix declares and consumes forgejo/secret_key, forgejo/internal_token, forgejo/jwt_secret, and forgejo/lfs_jwt_secret.
  4. Secret ownership is module-defined and must stay aligned with service users:
    • Forgejo secrets are owned by config.services.forgejo.user.
    • Step SSH provisioner secret is locked to root ownership and 0400 mode.
  5. flake.modules.homeManager.soteria imports homeManager.mysops and sets mysops.hostSecretFile to the same Soteria host secret file path. This drives helper tooling such as edit-secrets while Home Manager keeps its own default SOPS context from modules/programs/sops.nix.

Current implementation note:

  • restic_password exists in modules/hosts/soteria/secrets.yaml, but it is not currently consumed by the Soteria NixOS module graph.
  • Changing, deleting, or renaming seemingly unused keys in secrets.yaml should be treated as a compatibility change and confirmed by the task.

Restic REST Server On Soteria

Soteria's RESTic REST server is implemented by the shared nixos.restic-server module and configured by host-local resticServer values. Trace it this way:

  1. modules/hosts/soteria/default.nix imports nixos.restic-server and configures the host-local resticServer option set with:
    • enable = true
    • dataDir = "/mnt/restic"
    • privateRepos = true
    • listenAddress = "0.0.0.0:8000"
    • tls.certFile = config.mtls.certFile
    • tls.keyFile = config.mtls.keyFile
  2. modules/features/restic.nix maps those resticServer options into services.restic.server and appends TLS flags when both TLS paths are set.
  3. The shared module keeps the server base behavior in one place:
    • services.restic.server.enable = true
    • services.restic.server.dataDir = cfg.dataDir
    • services.restic.server.listenAddress = cfg.listenAddress
    • services.restic.server.privateRepos = cfg.privateRepos
    • services.restic.server.extraFlags = cfg.extraFlags ++ tlsFlags
  4. modules/features/restic.nix also opens the matching TCP port through networking.firewall.allowedTCPPorts, deriving it from resticServer.listenAddress.
  5. The mTLS renewal hook includes restic-rest-server.service in mtls.renew.reloadUnits, so certificate rotation reloads the REST server alongside Forgejo.
  6. loginText.extraServiceStatus exposes the running unit as restic-rest-server.service, which is the service name to keep in mind for status and reload behavior.

Storage Location

  • Soteria explicitly sets resticServer.dataDir = "/mnt/restic".
  • The shared module maps that value into services.restic.server.dataDir, so /mnt/restic is now the intended repository storage location for this host.
  • The shared module also derives the firewall port from resticServer.listenAddress, so Soteria no longer needs a separate networking.firewall.allowedTCPPorts = [ 8000 ] line.
  • If storage needs to move later, change the resticServer.dataDir input or the shared module contract, not ad hoc service cleanup logic.
  • privateRepos = true constrains repo exposure behavior, but it does not define storage location by itself.

Current implementation note:

  • The REST server is TLS-protected but currently passed --no-auth, so client access control relies on transport/security model rather than rest-server password auth.
  • The shared module default listen address is not Soteria's deployed value; Soteria deliberately overrides it to 0.0.0.0:8000 in the host.

Forgejo Implementation On Soteria

Soteria's Forgejo setup is split between host-local option values and the shared nixos.forgejo module. Trace it this way:

  1. modules/hosts/soteria/default.nix imports nixos.forgejo and enables it with:
    • forgejo.enable = true
    • forgejo.root_url = "https://forgejo.john-stream.com"
    • forgejo.https = true
    • forgejo.port = 443
  2. modules/features/forgejo.nix defines forgejo.port as the controlling option for the web listener. Its default is 3000, but Soteria overrides it to 443.
  3. The shared module wires that option into multiple places:
    • services.forgejo.settings.server.HTTP_PORT = cfg.port
    • networking.firewall.allowedTCPPorts = [ cfg.port ] when openFirewall = true
    • If cfg.port < 1024, the Forgejo systemd unit gets CAP_NET_BIND_SERVICE so it can bind a privileged port.
  4. HTTPS on Soteria is terminated directly by Forgejo using mTLS-managed files:
    • services.forgejo.settings.server.PROTOCOL = "https"
    • KEY_FILE = config.mtls.keyFile
    • CERT_FILE = config.mtls.certFile

Storage Locations

  • This repo does not override Forgejo's primary state directory in the Soteria host or in modules/features/forgejo.nix.
  • That means Forgejo storage follows the underlying NixOS services.forgejo defaults, exposed in this config as config.services.forgejo.stateDir.
  • PostgreSQL storage is also left at the underlying module default and is referenced as config.services.postgresql.dataDir.
  • The cleanup helpers in modules/features/forgejo.nix confirm these are the intended storage anchors because they remove exactly config.services.forgejo.stateDir and config.services.postgresql.dataDir.
  • Forgejo backup dumps are generated by forgejo-dump.service using --work-path ${config.services.forgejo.dump.backupDir}, so dump staging/output follows the Forgejo module's backup directory setting rather than a Soteria-specific path override.

Current implementation note:

  • If you need to change Forgejo storage location on Soteria, do it by setting the underlying Forgejo or PostgreSQL service directory options explicitly, not by changing cleanup scripts alone.
  • If you need to change the external Forgejo endpoint, update forgejo.root_url, forgejo.port, and the Soteria mTLS SAN list together.

Change Safety Rules

  • Do not rename the secret key janus/admin_jwk without updating all consumers.
  • Do not change SSH host key/cert filenames unless OpenSSH HostKey/HostCertificate paths are updated together.
  • If changing hostnames or domains, update cert principals in ssh-certs and corresponding SSH client targets together.
  • If adding or removing nixos.ssh-certs, explain impact on SSH cert issuance, renewal, and trust in the change summary.