Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

docs/v2/08-security.md

8. The security model

From monetize main c9b473a5. SECURITY.md at the repository root is the policy (how to report, the 14/60/90-day clock, what is supported); this chapter is the model the code implements.

8.1 The one sentence

The ONE box dials nothing but the cloud providers, the payment vendors, the DNS zones and its own release bucket; everything else dials it, signed; every plugin runs in a sandbox that holds no credential; and every number is a fold of an append-only ledger.

8.2 The doors

door who may use it what it carries where
the edge :80/:443 the internet /callback/<kind>/<instance> (the vendor's webhook, verified by the vendor plugin through the host's hmac door); POST /attach/enrol (gated by a one-shot token) and /attach/installed; the five estate-facing gRPC services Fleet, Signup, EstateOrder, Purchase, Products to the server over h2c; any other monetize.* or grpc.* service → 404; the retired /kop and /shop → 404; everything else to the console. A bearer is carried as it is for monetize's interceptor to judge crates/monetize-admin/src/edge.rs:1-37, 74-106
the operator's door :8475 the operator only: TLS 1.3 with a client certificate from the box's installer CA, CN installer; anyone else 403 [not-the-operator]; handshake 10 s; body 64 MiB crates/monetize-admin/src/door.rs:1-3, 50, 2016-2023
the installer link (WireGuard, udp/51821) the kit's installer, for life the install, a keep boot, the re-feeds private-monetize-ops/terraform/main.tf:131
monetize-server 127.0.0.1:50061 the box's own processes "nothing outside this box dials it" crates/monetize-front/src/intake.rs:52-54
the console 127.0.0.1:8091 the edge the operator's pages crates/monetize-admin/src/solo.rs:13-15

The terraform range for the door and the installer link defaults to the whole internet (variables.tf:42-52): mutual TLS is the protection, not the firewall.

8.3 Who is who: the bearers and the keys

credential minted held by opens
the service bearer <data>/monetize/token 32 random bytes, hex, once at first start (intake.rs:1069-1084) the console's backend, PID 1 monetize's loopback gRPC
the operator bearer <data>/monetize/data/operator-token 32 random bytes, never equal to the service bearer (intake.rs:1086-1099) the door, the ONE box's console refunds, settles, site orders, new estates, test buys, plugin updates, UpdateEstate, CODE RED; an empty operator bearer means those are CLOSED (crates/monetize-server/src/auth.rs:22-80)
an estate's ONE bearer at enrolment (POST /attach/enrol, a one-shot att1. token; 32 random bytes; only its digest is kept; 365 days) the estate's front Signup, Purchase, EstateOrder for that estate; ALERT attach.bearer_expiring a few days before it ends; revoked with attach-revoke
the console's control key the console's own SSH key (--control-key) when it runs OFF the box: per-call SSHSIG bound to the method, ±5 min, a nonce, single use; the server holds only the public key in control_principals the console the operator's calls from outside
the break-glass token console_admin_token by the kit at install, 32 random bytes, 0600, never printed the kit's vault enrolling the FIRST passkey (and again after POST /passkeys/reset)
passkeys WebAuthn in <data>/monetize-ui/state.redb the operators the console; every write needs a fresh touch (step-up)
monetize's signing root signing.key 32-byte Ed25519 seed, minted once, 0600; one derived key per product: seed = SHA-256("nordisk monetize product signing key v1" ‖ 0 ‖ root ‖ product); the root signs nothing itself (crates/monetize/src/sign.rs:222-262) the box every fact, order and growth grant; the public half leaves as monetize_pubkey
a box's fleet key <data>/fleet/box.ed25519 on the box the box; pinned at the ONE box on its first call inside a 10 min window (chapter 5 §5.3) Fleet.Report/Orders/Ask; a forged call is never acked
the estate client key, the installer material (a per-order CA, the installer and bootstrap certs, the WireGuard keys, the operator key pair; 180 days) at payment, sealed in the custody the custody; the front takes the material after the install the front's asks; the installer link
the plugin signing key plugin-signing.pk8 + .der the release manager's machine (--sign-with, $DWARVES_PLUGIN_SIGNING_DIR) never on a box every plugin's .p7s and the signed plugin list; the box verifies against guest_trust_roots
the korp root seed the operator's machine (~/.dwarves/root/korp-root.seed) never on a box; the box holds the enrolled public root in its ESP the media and every live app (stage_on verifies against the enrolled root, the rollback floor and the BASE)
the vendor's keys and the webhook secret; the cloud and DNS tokens fed by name as secret.<name> → <data>/monetize/secrets/secret.<name> (0600, the directory 0700), re-read at every use the host only added per request by the http door from [[http_auth]]; compared by the hmac door; a guest never sees a value
a box's host, TLS, SSH and forge keys on the box the box; only public halves leave it (crates/monetize-server/src/custody.rs:29-30)

Sealed secrets: each private estate's secrets are sealed in custody/<company>.sealed with AES-256-GCM under custody/kek (0600), the company name in the additional data (custody.rs:1-40, seal.rs:1-11); the off-site copy is age-sealed to the operator's X25519 recipients (monetize-admin/src/offsite.rs); an age1yubikey… recipient is refused by the kit.

8.4 The plugin sandbox

Chapter 3 §3.0 has the numbers. The rules:

8.5 What crosses between the box and an estate

8.6 The buy doors

8.7 The console's hardening

A cookie-authenticated non-GET with a missing or foreign Origin is 403 (CSRF); no-store on /api and /auth; nosniff, DENY framing, no-referrer, COOP/CORP, a permissions policy and a strict CSP with form-action 'none' (S/serve.rs:225-299). Pages may not show configuration words (monetize.toml, MONETIZE_UI_), held by a test. The console backend has no rate limiter of its own.

8.8 What an operator can and cannot do

Can: everything the console offers, with no roles; at the door: feed, attach tokens, revoke, restore, the box's update, every site verb, the product's own orders.

Cannot: get a shell (the box runs a tunnr kernel with none); read a secret value back (GET /feed answers names only; site show shows "set" and a fingerprint); replace a pinned box key (site open-pin opens only unpinned roles); test-order on a live key; terminate without typing the name back, or a site not marked terminable; refund or settle without the operator bearer; edit the bootstrap-only items (the plugin root, the shop's cloud and DNS tokens) in the app; get the release signing key or the korp root onto the box.

8.9 Mail

TLS always: smtp:// means STARTTLS required, port 465 or smtps:// implicit TLS, ?tls=opportunistic is refused, verified at start (crates/monetize-mail/src/lib.rs:1-50). The alert mail goes to secret.alert_to; the buyers' mail per product through secret.sold_smtp_url.<product>.

8.10 Supply chain

cargo-deny reads every lock against RustSec; an ignore in deny.toml states its reason and date and is mirrored in osv-scanner.toml (held equal by xtask/tests/osv_triage.rs); updates are proposed weekly by the forge, advisories at once, images and actions pinned by digest; fuzz/ holds cargo-fuzz targets for the parsers that read untrusted bytes (vendor callbacks, the feed and monetize.toml, signed facts, the Fleet door, a box's orders), run every night. Every plugin that ships is pinned by sha256 in the kit's solo.toml and checked by cargo xtask pins --strict before a feed.