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:
- A module runs only if a detached CMS signature over its BLAKE3-256 chains to a DER root the block names; a block with no roots is
[guest-unsigned], and a pinned digest alone is not enough. The signature is checked before the module is compiled. - The linker defines exactly the doors the block's
grantsunlock, and each kind may hold only its own grants (a product: none; a vendor: http, hmac, state; a cloud: http, state; a DNS writer: http; a signup guest: grpc, state). An import outside the vocabulary, or a retired door (sign,env), refuses the module. Gate A: every grant the manifest asks for is granted. Gate B: every import the module really has is declared. - The
httpdoor dials only the block'shttp_hosts, follows at most 5 redirects within them, caps bodies at 16 MiB, and adds the credential itself. Thehmacdoor compares in constant time, over at most 8 candidates, against a secret named inhmac_secrets. Thestatedoor holds 4096 keys of 64 KiB (4 MiB for a cloud). Thegrpcdoor reaches onlyguest_grpc_hostsand presents the product bearer itself. - Fuel, a wall-clock deadline, a memory cap and a table cap bound every call; a block may lower them, never raise them past the kind's ceiling; zero is refused, never unlimited.
- The spend cap wraps every cloud that spends and is enforced on the host;
destroyis never capped. - A refused replacement keeps the serving guest; a refused feed keeps the config in force.
8.5 What crosses between the box and an estate
- Monetize never calls into an estate. A box dials
Fleet.Report,Fleet.Orders(a long poll of at most 25 s) andFleet.Ask, each signed with its fleet key under the domainnordisk-fleet-call-v1, fresh within 5 min (crates/monetize-embed/src/fleet.rs:21-30). - Every order is signed with the product's derived key under
nordisk-order-v1, names the box's role, carries a strictly risingseqthe box persists before it runs the order, and a validity window; the box refuses a forged, foreign, old, expired or future order (crates/monetize-estate-side/src/pump/puller.rs:181-225). Arefeedorder may carry a secret and is never logged; asign_in_againorder carries none. - A fact is verified offline on the product: V1 Ed25519 over the canonical JSON, V2 with the issue time; a fact not newer than the one held is refused (chapter 6 §6.5).
- The Guardian repairs by queuing signed orders and by the cloud API; the fence invariant (no lay for a role until its old box is read back gone; the old key forgotten) is checked before and at every lay step (chapter 5 §5.8).
- The attach door:
POST /attach/enrolredeems a one-shot token (24 h by default, 10 min from the kit), signs the estate's CSR under the attach CA ("monetize attach", 10 years), mints the estate's bearer and writes the joined record only after the token is spent (crates/monetize-front/src/attach.rs:237-370).POST /attach/installedis verified against the owner key recorded at enrolment.
8.6 The buy doors
- The site relays with its ONE bearer; the ONE box refuses a call that names no product, an unknown site, or a visitor's order with no account plan, before any money.
- Rate limits at the site (
crates/monetize-site-buy/src/limit.rs:1-21, 136-161): signup 120 a minute per session plus 30 new, the name check 10 plus 30, the private doors 60 plus 30; a site'sallowedmay refuse earlier with 429.X-Forwarded-Foris believed only from loopback and the site'strusted_proxies. - The vendor page's return URLs are on the site's origin; a non-https URL is refused (502).
- The webhook: at most 256 KiB, 32 in flight;
Unverified401; a handle mismatch 409. - A buy that needs a fresh login (a renewal, a change, a terminate) asks the site's
stepped_up(a user-verified login in the last 10 minutes); an unproven one is refused.
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.