Defense-in-depth notes for the Surmount mail VPS. Design notes: open-choices.md. Secrets detail: SECRETS.md. Data plane: DATASTORES.md. Ops: OPS.md.
Last updated: 2026-08-20 (extra mailbox domains get the same mail-auth DNS and the same hosted DNSSEC as the primary; leftover parent DS is a SERVFAIL class to clear then sign, not leftover-unsigned). Prior 2026-08-19 (sshd host keys: module advertises/generates ed25519 only; live host still offers RSA until a deploy. Journald paper trail size-capped; request log still omits Authorization/Cookie; Onion-Location + Alt-Svc on Axum HTTPS; live Arti unit active; Tor Browser verify still residual)
This document is posture and design, not a penetration-test report. Open implementation details are marked honestly. Not operator-accepted unless you say so.
Enabling already-shipped modules on a real box is operator host work with
proof gates. Sample #mail-vps keeps public HTTPS, requireDeployMaterial,
Arti, ban enforce, and production Nostr commented so eval needs no host
PEMs. Ordered stages (B0 preserve SSH access through B7 shrink transitional
fail2ban), proof columns, and D1 hybrid TLS honesty:
deploy-host-local.md section 6. Day-one automation:
OPS.md. Residual host rows: RESIDUAL.md.
- Run mail and product HTTPS on hardware/VPS we control, rebuildable from a flake, without a required third-party reverse-proxy CDN.
- Separate deploy secrets, human vault, and disk encryption so none is asked to do the others' job.
- Authenticate product operators/users with Nostr cryptography at our edge; keep Stalwart as the mail engine with ordinary mail credentials.
- Prefer LUKS2 at rest when install allows; harden the live system regardless.
- Live Vaultwarden unit / first admin user (S7b host residual after token;
S7a module offline done; A0
host-cutover --with-vaultwardenscripts install kind + enable fragment; sample host stays enable=false). - Full public / onion exposure design for VW (operator residual; no invented Q-ARTI answers).
- Actually reformatting a live VPS with LUKS this turn.
- Claiming FDE stops a malicious cloud hypervisor with live memory access.
- Claiming Vaultwarden encrypts Stalwart/RocksDB mail data.
| Threat | Mitigations (layered) | Residual |
|---|---|---|
| Stolen disk / volume snapshot offline | LUKS2 FDE; provider volume encryption if any | Hypervisor live peek; bad unlock key handling |
| Git leak of repo | Zero secret material in public tree (plain or ciphertext); secrets host/out-of-band only | Operator accidentally commits secrets; still scrub and rotate |
| Laptop age key theft | Operator OPSEC; optional separate deploy keys; Vaultwarden for day-to-day human secrets | If age key + host ciphertext both leak, deploy secrets burn |
| Stolen VPS root while running | SSH keys only; fail2ban; minimal ports; no CF as excuse for weak origin | Rootkits; 0-days; weak app auth |
| Credential stuffing on web UI | Nostr signatures (not password reuse); rate limits; allowlists | Stolen nsec; XSS stealing session |
| Mail protocol abuse | Stalwart limits, greylist/spam, fail2ban careful expansion | Volume floods; reputation if open relay misconfig |
| Backup exfil | restic encryption; separate restic password in sops + offline copy | Backup repo ACL mistakes |
| Supply chain (nixpkgs, crates) | flake.lock pins; prefer minimal deps; review upgrades | Upstream compromise |
| Legacy site RCE | Static files only; no PHP for old content | Edge bugs; upload pipeline mistakes |
| CDN/MitM dependency | No required Cloudflare hop; our TLS | Operator DNS mistakes |
See EDGE_AND_TLS.md and open-choices.
- A/AAAA for mail and services point at the VPS.
- We own TLS termination, rate limits, and basic abuse controls.
- DNS may still use any registrar (including Cloudflare DNS-only).
- Do not design features that only work behind CF Access/Workers/Tunnel/WAF.
- Public: 25, 465, 587, 993, 4190, 80/443 (and SSH on a chosen port).
- Loopback only: management-ui, Stalwart HTTP, Vaultwarden (when enabled; default rocket 127.0.0.1:8222; world bind needs allowNonLoopbackListen).
- Edge: nginx transitional-to-delete -> Rust target (operator direction 2026-07-30); UDS preferred for local backends. See EDGE_AND_TLS.md.
- Hardening:
modules/hardening.nix(SSH, light fail2ban; optionalaccessControlnft sets). When hardening is on, sshd hostKeys is exactly one ed25519 key at/etc/ssh/ssh_host_ed25519_key. The daemon used to also offer RSA (NixOS 26.05 default is rsa-4096 + ed25519). Clients already preferred ssh-ed25519. This module stops generating and advertising rsa. Live deploy is leftover: the running host still advertises RSA until adeploy-hostswitch. Dropping rsa fromhostKeysdoes not delete leftover RSA files on disk. PQ honesty: dropping RSA host keys is classical SSH hygiene. Ed25519 is not post-quantum. OpenSSH host keys are not PQ. PQConnect is a separate userspace path; it is not a host-key swap. Do not claim this change is PQ. Ban path: rate limit + ban decide (ban.rs), enforcement default off. Target remains merciless ban + whitelist + last-used; Q-ACL-* open. See research/access-control-fail2ban.md and EDGE_AND_TLS.md operator end-to-end recipes (nix run .#e2e/nix run .#e2e-host, orjust e2e/just e2e-host). Not a full WAF and not Cloudflare-dependent. - PQC: management-ui rustls + aws-lc-rs with
prefer-post-quantumoffers hybrid X25519MLKEM768 first among default kx groups (D1; hermetic unit proof of provider groups, not automatic live-host hybrid negotiation). Host probe after B1:nix run .#surmount-tls-hybrid(envSURMOUNT_E2E_BASE_URL; exit 2 BLOCKED if unset; not CI cutover). PQConnect is a separate path-layer track (D2; not a substitute for D1). Research: research/pqconnect-and-pqc.md; dual-pin EDGE_AND_TLS.md, deploy-host-local.md. - Mail auth classical dual-sign (operator direction 2026-08-11): Day-1
DKIM is Ed25519 + RSA-4096 (selectors
stalwart/stalwart-rsa). Best algorithms receivers accept today. Neither is post-quantum; larger RSA does not buy meaningful quantum resistance (Shor). Do not claim PQ from RSA-4096. When IETF/receivers support PQ or hybrid mail auth, Surmount tracks that residual separately from D1 TLS hybrid and D2 PQConnect. Checklist and paths: DNS.md Hardness and quantum honesty. - Mail records on every mailbox domain (operator 2026-08-20): extra
domains this host sends or receives through (examples:
cryptoquick.com,baxterartworks.com) get the same mail-auth DNS as the primary, including primary-class DNSSEC (Namecheap hosted DNSSEC Status ON; ECDSA P-256 SHA-256 algorithm 13 acceptable for now): working Namecheap hosted DNS, SPF, dual DKIM TXT, DMARC at the operator-directed policy (live mailbox_dmarcisp=quarantine; do not usep=reject; Baxter intended isp=quarantine; public_dmarc.baxterartworks.commay stay NXDOMAIN while EmailType is FWD), TLS-RPT, CAA, MTA-STS on names this cert and this host actually serve. Do not harden onlysurmount.systems. A leftover parent DS with no matching child DNSKEY is a DNSSEC honesty fail: validating resolvers SERVFAIL the zone (cryptoquick.comwas that class). That leftover is a SERVFAIL bug to clear then sign, not a reason to leave the zone unsigned. We asked cryptoquick OFF only because leftover DS 2368 (alg 13, digest type 1 SHA-1) had no DNSKEY. Operator wants ON same assurmount.systems. Do not re-add 2368 by hand. DS digest type 1 (SHA-1) is also a fail, even if a DNSKEY exists. Static-site-only extra vhosts are not automatically mail domains. Public MX on registrar eforward is a fail for a claimed mailbox (domain-audit;--live set-mxis not a flip while EmailType is FWD). Dual-pin: DNS.md, COMPACTION-PIN.md, ../AGENTS.md. - Arti onion/hidden services (REQUIRED): services must be reachable via
Arti HS alongside clearnet; HS keys never in git; not a clearnet edge
replacement. Clearnet HTTPS advertises the onion with Onion-Location
and Alt-Svc (apex, www, services, extra static Hosts, MTA-STS;
same v3;
/_o/{host}on non-console Onion-Location; process-start load; restart after hostname/env/map/static-vhost change). Live unitsurmount-arti-hidden-serviceis active (2026-08-17); Tor Browser verify and operator HS backup remain residual. Do not claim B3 fully closed. research/arti-and-secrets-manager.md, COMPACTION-PIN.md section 7, EDGE_AND_TLS.md.
Proposed direction (open-choices). Product mode for operators is Nostr at the Axum edge (not HTTP Basic, not a password farm).
| Situation | Required |
|---|---|
| Public product HTTPS / non-loopback operator console (services Host) | authMode = "nostr" + host session secret + allowlist |
Loopback / lab / just dev |
authMode = "off" allowed (open console; not public-safe) |
| Apex / www Host | Public packaged SurmountSystems/site (not the console); orthogonal to Nostr gate |
When mode is off, middleware does not gate routes: anonymous visitors see full console HTML and inventory APIs. That is lab only. Do not leave the live services Host on auth-off.
Host enable (B4): install session-secret and nostr-allowlist on Domain
B, set private host-local authMode = "nostr" (plus paths and
publicBaseUrl), switch, prove login redirect / API 401 / health 200. Full
operator runbook and proof curls: OPS.md section Production Nostr
auth on the public edge (B4). Secrets shapes: SECRETS.md.
Edge wording: EDGE_AND_TLS.md.
Footgun guard (product): public primary edge + auth-off is refused
(binary + Nix assert). Lab/CI keep auth-off on loopback.
Live B4 (2026-08-12): public services is Nostr-gated (anon / login,
/api/v1/domains 401, /health 200). Apex/www serve packaged
SurmountSystems/site (2026-08-18).
Q-AUTH-1 still open. Report: .agents/reports/impl-auth-live-b4-switch.md.
- Operators (later users) prove control of a Nostr keypair (npub public, nsec private, never stored by us).
- HTTP APIs prefer NIP-98: ephemeral event
kind
27235, tagsu(absolute URL) andmethod, optionalpayloadSHA-256, shortcreated_atwindow; headerAuthorization: Nostr <base64(event)>. - Foundation (2026-08-01): management-ui verifies NIP-98 with rust-nostr
(
nostrcrate; not JS NDK). Scaffold session is an HMAC-signed HttpOnly cookie (SURMOUNT_SESSION_SECREThost-only). Allowlist viaSURMOUNT_NOSTR_ALLOWLIST(env) or optionalSURMOUNT_NOSTR_ALLOWLIST_FILE(same parse rules; env wins when non-empty). Empty allowlist is fail-closed. Modeoffdefault for local dev / loopback only. Full Q-AUTH-1 (durable store, bootstrap UX, key-loss) still residual (do not invent product answers here). - After login, sessions (cookie or server-side) are expected so SSR pages are usable without signing every GET (exact design open).
- NIP-42 is relay AUTH; relevant if we speak relay protocol, not a drop-in browser session by itself.
- Holds mail accounts and protocol auth (passwords, app passwords, OAuth device flows as configured).
- Supports external OIDC/LDAP/SQL directories (OIDC backend) and can act as OIDC provider/client. That is not the same as Nostr.
- Does not natively verify Nostr npub signatures unless we build a bridge.
The management UI does not hash mailbox passwords in the browser or in
Axum. POST /api/v1/accounts/password sends the plaintext secret only over
the already-authenticated operator session to loopback Stalwart
(x:Account/set credentials.0 @type Password). The engine hashes.
Stalwart 0.16.15 (this repo pin in nix/packages/stalwart-mail.nix) uses
Authentication.passwordHashAlgorithm. The enum and singleton default are
Argon2id. Source (tag v0.16.15):
crates/registry/src/schema/enums.rs:PasswordHashAlgorithm#[default] Argon2idcrates/registry/src/schema/structs_impl.rs:impl Default for Authenticationsetspassword_hash_algorithm: PasswordHashAlgorithm::Argon2idcrates/directory/src/core/secret.rs:hash_secretforArgon2idcallsArgon2::default().hash_password
Public docs (same default; accessed: 2026-08-14):
Passwords and
Authentication.passwordHashAlgorithm
(argon2id | bcrypt | scrypt | pbkdf2).
Policy runs at set time. Existing empty mailbox credentials stay empty
until the operator sets a password. Changing the algorithm later does not
rewrite stored hashes; IMAP still verifies known prefixes ($argon2, and
others listed on the Passwords page).
Optional host pin (not auto-applied; default is already Argon2id):
stalwart-cli update Authentication --field passwordHashAlgorithm=argon2idVerify: stalwart-cli get Authentication and confirm
passwordHashAlgorithm is argon2id.
Browser --NIP-98/session--> Axum/Leptos --maps npub--> role + mailbox
|
+-- service creds / managed tokens --> Stalwart
MUA --app password--> Stalwart (IMAP/SMTP) [issued via admin after Nostr login]
Open: first-operator bootstrap allowlist, key-loss recovery, whether Surmount ever becomes an OIDC IdP that Stalwart trusts.
Full write-up: SECRETS.md. Summary:
| Layer | Tool | Protects | Does not replace |
|---|---|---|---|
| A. Deploy | sops-nix + age (host-local) | Service secrets on host at activation; never in public git | Human UX vault; FDE |
| B. Human vault | Vaultwarden (S7a offline done; S7b scripted by host-cutover after token) | Operator passwords/TOTP/notes | Deploy secrets at rebuild; mail store crypto |
| C. Disk | LUKS2 | Offline disk/snapshot; unlock material never in git | Running OS compromise |
NEVER secrets in git (plain or ciphertext). hygiene.md.
agenix: not dual-stacked; sops-nix primary only.
Vaultwarden (upstream):
- Surmount wrap:
surmount.vaultwarden.*over stockservices.vaultwarden(default enable false on sample host). Offline S7a shipped; S7b enable path scripted by A0 host-cutover after token (live unit host-gated). Detail: SECRETS.md section 5. - Defaults: SQLite, loopback Rocket (
127.0.0.1:8222),SIGNUPS_ALLOWEDforced false,configureNginxrefused (Axum-first edge; no nginx product edge for VW). - Public path (optional):
managementUi.vaultwardenProxyEnablereverse- proxies/vault/(configurable) on the Axum edge to loopback Rocket. Vaultwarden login is SoT on that path day-one (no Nostr gate). WebSocket Upgrade is forwarded. Admin CSP is not applied to proxied VW responses (frame denial still applied; do not iframe VW into the console). Prefer after public https edge is real. Console Open vault becomes same-origin path when proxy is on. Default off on sample host. - Without path proxy: console still surfaces operator-published URL / loopback derive as an external link ("not reverse-proxied here").
- Admin token: host EnvironmentFile path only
(
adminTokenEnvFile, e.g./run/surmount-secrets/vaultwarden/admin.envvianix run .#secrets-install-host). Never inline in Nix config /extraConfig(stock config is store-bound and world-readable). Not "sops-inline secret string" as the product path. Never log ADMIN_TOKEN through the path proxy. - Backups of
/var/lib/vaultwardenare critical when live (restic paths are operator residual until enable). - Chicken-and-egg: needs domain B host file to start; cannot supply secrets to pure flake eval; never activation feed for other units.
sops-nix does not unlock LUKS2. Deploy-secret decrypt runs at activation after root is mounted. Research (patterns, chicken-and-egg): research/luks2-and-deploy-secrets.md.
NixOS supports LUKS/LUKS2 well:
boot.initrd.luks.devices.*- Full Disk Encryption wiki
- Remote disk unlocking (SSH/dropbear in initrd)
- disko for declarative partition+LUKS layouts
- nixos-anywhere can upload disk encryption keys at install (howto)
systemd-cryptenrollfor TPM2/FIDO2 where hardware exists
-
Many provider "NixOS images" boot unencrypted root.
-
FDE usually means custom install: nixos-anywhere + disko, custom ISO, or rescue-environment reformat (downtime).
-
Most VPS lack a useful TPM for unattended unlock you control.
-
Headless unlock choices:
Method Pros Cons Passphrase via initrd SSH Simple, well documented Manual after every reboot/crash; network must come up in initrd Tang/Clevis Unattended if Tang up Extra infra; network trust story TPM2 enroll Unattended on bare metal Rare/absent on VPS; PCR fragility; weak vs stolen whole machine Host-local keyfile on unencrypted boot Convenience Often weaker (key next to ciphertext); never from git sops-nix decrypt of keyfile on root N/A for root unlock Too late; root already must be open Any unlock material in public git Forbidden Plain or ciphertext; do not suggest
- Prefer providers/workflows that allow LUKS2 root via disko + nixos-anywhere (or equivalent) on greenfield or planned reinstall.
- Plan initrd SSH + passphrase unlock as the default remote story (Option 1) unless better hardware appears; document NIC modules and static/DHCP in initrd.
- Keep LUKS passphrases / recovery keys offline (paper, USB, human vault not only on this disk). Never in git (plain or encrypted).
- Treat provider "encrypted volume" checkboxes as optional extra, not a substitute for OS LUKS we understand.
- Interim: running on an unencrypted provider image is allowed with residual risk accepted until reinstall; do not pretend FDE exists.
- Do not claim sops-nix configures FDE or unlocks root.
- Do not store LUKS keyfiles as sops ciphertext in the public repo.
Does: raise cost of offline disk theft, abandoned disks, many snapshot exfil scenarios when the volume is locked.
Does not: stop root on a running system, malicious host ops with memory access, ransomware already executing as root, or replace application auth.
Accurate to code in crates/management-ui/src/main.rs (not aspirational
Q-ACL-1 surface answers). Enforcement still depends on
SURMOUNT_BAN_ENFORCEMENT (off = signal ignored, dry-run/enforce =
record; whitelist never banned). Hermetic tests:
surface_audit_404_and_501_do_not_auto_ban,
auth_failure_ban_matrix_signals_and_skips.
| Surface | Condition | Calls signal_unauthorized? |
|---|---|---|
POST /api/v1/auth/session |
Event parse fail (missing/malformed body or Authorization) | Yes |
POST /api/v1/auth/session |
NIP-98 verify fail (sig, allowlist, skew, u, method) |
Yes |
| Protected HTML/API (mode=nostr) | Missing session cookie and no Authorization: Nostr |
No (gate only: redirect /login or 401) |
| Protected path | Authorization: Nostr present and error is BadSignature, NotAllowlisted, WrongKind, Skew, UrlMismatch, or MethodMismatch |
Yes |
| Protected path | Authorization: Nostr present but unparseable (Malformed) |
No (log gate only; not in ban match list) |
| Unknown HTML path | mode=nostr: temporary redirect to /login (gate); mode=off: 404 |
No |
Unknown /api/... path |
mode=nostr without credentials: 401 gate | No |
POST /api/v1/jmap |
501 when reached (auth off or authenticated); under mode=nostr without credentials the gate returns 401 first | No ban either way |
GET /health |
Always public | No |
Structured logs on auth fail use surface + auth_error_kind only (no cookie,
nsec, Authorization, or event JSON). Full Q-ACL-1 (which other mail/HTTP
surfaces must raise the hook) remains open.
- Nostr auth + short-lived NIP-98 windows; bind session to expectations (HTTPS, Secure cookies, sensible SameSite) when sessions exist.
- Session cookie (shipped scaffold):
surmount_sessionis HttpOnly, SameSite=Lax, Path=/; Secure is set when listen mode is HTTPS (secure_cookiesfromlisten_mode.is_https()). SameSite=Lax already blocks most cross-site POSTs from carrying the cookie. - CSRF (shipped): cookie-authenticated mutations use double-submit
except mailbox password, create mailbox, and grant-console. Session
exchange and authenticated
GET /mailset HttpOnlysurmount_csrf(Path=/, SameSite=Lax, Secure when HTTPS) as a belt.GET /mailembeds a session-bound token (HMAC of the session cookie with purposesurmount-csrf-session-v1) in#mailbox-csrf/#mailbox-create-csrf/#mailbox-grant-csrfand isCache-Control: no-store.POST /api/v1/accounts/password,POST /api/v1/accounts, andPOST /api/v1/accounts/consoleverifyX-CSRF-Token/ JSONcsrfagainst that session HMAC (constant-time). The CSRF cookie may be absent; session + matching hidden/header token is enough. Session + empty/wrong token is 403. No session is 401.POST /api/v1/auth/logoutstill requires double-submit cookie+header.PATCH /api/v1/accounts/{id}still requires double-submit when a session cookie is present (NIP-98-only or lab auth-off escape skips CSRF). - Create mailbox is Administrator only (2026-08-14). No public signup.
Auth-off create is 403 without the lab directory escape. Console User
is 403 even if they POST the form. Stalwart create is always engine
User (never Stalwart Admin from the portal). Reserved local-part
adminis refused. Map file/var/lib/surmount/console/accounts.jsonis ownersurmount-uimode 0600; writes are temp-then-rename; existing symlink, directory, or world-writable inode is refused. Role is re-read from the map on every request. Cookie stays{ sub, exp }only. Host allowlist keys with no map row stay Administrator. User npubs are not copied to the allowlist. - Attach npub is Administrator only (2026-08-20).
/mailGrant console login postsPOST /api/v1/accounts/consolewith session-bound CSRF. Bech32npub1...(or hex) is validated; garbage and nsec are refused without echoing the token. Directory listing may stay unavailable; the bind writes the console map so that npub can log into the services portal (AuthMode nostr). User npubs stay in the map, not the host allowlist. Q-AUTH-1 is unchanged. - Baseline security headers (shipped on management router):
Content-Security-Policylean for SSR admin (default-src 'self', no third-party hosts;script-srcwith per-request nonce for/loginNIP-07 inline script;style-src 'self' 'unsafe-inline'for DOGE CSS;frame-ancestors 'none'),X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY. Onion-Location + Alt-Svc (2026-08-17, every public Host 2026-08-20): emitted on mapped HTTPS 2xx/3xx (apex, www, services, extra static Hosts, MTA-STS; same v3;/_o/{host}discriminator for non-console). Not on.onionHost, not on the:80redirect router, not on the local Arti cleartext bind. Mail unmapped. 4xx/5xx emit nothing. Mapping is loaded at process start (no hot-reload). Dump onGET /api/v1/systemonion_discovery(admin-gated when Nostr on). Header presence is not a Tor Browser Alt-Svc upgrade proof. - Full webmail CSP / attachment sandbox remains residual (hostile email HTML).
- Rate-limit auth endpoints at edge and app.
- Do not expose Stalwart admin on the public internet long-term.
- Attachments: size limits, content-type caution, no drive-by exec paths.
- Operator-only request and TLS logs (journald). The Axum edge writes
structured
http_requestandtls_handshake_failedlines to journald (surmount-management-ui; info on stdout, WARN+ on stderr). They are not world-readable and are not dumped on any HTTP route (/healthand/api/healthstay status JSON only). Do not add a public log viewer or write access logs under/var/log//var/lib/surmount. Operators read viajust host-logsorjournalctl -u surmount-management-uiover SSH. Fields omit query content, cookies, Authorization, PEM, and cert bytes. Peer IP is a journal field only. Persistent journal is size-capped (surmount.logging; scaffold SystemMaxUse 1G, not a guest disk SKU). Never log passwords, tokens, or HS keys. Detail: OPS.md Logging.
- restic (or equivalent) for
/var/lib/stalwart-mail, Vaultwarden data, and other state; password from sops. Prefer stop-service or FS snapshot consistency for RocksDB (DATASTORES.md). - Separate offline copies of: age identities, LUKS recovery, restic password, Vaultwarden admin recovery.
- Test restore drills; a backup never restored is unproven.
+---------------------------+
| Operators / users |
| - Nostr nsec (client) |
| - Vaultwarden clients |
+-------------+-------------+
|
+-----------------------+-----------------------+
| HTTPS (our edge) | (human apps)
v v
+------------------+ +------------------+
| Axum + Leptos | optional /vault/ path | Vaultwarden |
| Nostr verify | proxy to loopback Rocket | (Bitwarden API) |
| admin + webmail | (or console link only) | passwords/TOTP |
+--------+---------+ +--------+---------+
| service tokens / JMAP ^
v |
+------------------+ host EnvironmentFile
| Stalwart | (domain B ADMIN_TOKEN)
| mail + RocksDB | never store-bound secret
+--------+---------+
|
=========+========== LUKS2 (preferred) wraps block device
| root FS + data |
| deploy decrypt-> | age key on host; secrets host-local only
| /run/secrets |
===================
public git: ZERO secrets (plain or ciphertext)
LUKS unlock material: never in git
no required Cloudflare hop
Arti onion/HS REQUIRED (alongside clearnet; live unit + discovery
headers 2026-08-17; Tor verify / HS backup residual)
single VPS (multi-host deferred)
| Path | Role |
|---|---|
modules/hardening.nix |
SSH (ed25519 host key only; no rsa generate/advertise) / transitional fail2ban sketch; merciless target in header |
modules/networking.nix |
Firewall ports |
modules/secrets.nix |
sops-nix wiring |
modules/web.nix |
edge TLS (transitional nginx) |
| SECRETS.md | Full secrets story |
| OPS.md | Logs, scripts, runbooks |
| EDGE_AND_TLS.md | Edge/TLS/no CF; Arti HS required |
| COMPACTION-PIN.md | Compaction reload pin |
| research/luks2-and-deploy-secrets.md | sops vs LUKS |
| research/pqconnect-and-pqc.md | PQConnect + TLS PQ |
| research/arti-and-secrets-manager.md | Arti HS + VW vs SM |
| research/access-control-fail2ban.md | Ban/whitelist design |