Owner: MeterBox SRE. Review cadence: whenever a Managed-tier capability changes (KMS provider, audit transport, helm chart, break-glass model).
This document is the operator-facing companion to
docs/on-prem.md. The on-prem doc tells the customer what each
tier inherits; this runbook tells the SRE operating a Managed (sovereign)
cluster (or the customer's security team reading along) how the three
Managed-tier promises are actually run:
The Managed tier is Self-Managed run by MeterBox SRE in the customer's sovereign region — same helm chart, same images, same APIs. What's different is who holds keys, where the cluster lives, and how operator access is audited. Everything below assumes that posture.
The customer picks one region from the supported sovereign clouds (AWS GovCloud, GCP Assured Workloads, Azure Government, EU-only AWS, or on-prem with the customer's compute). MeterBox SRE provisions one EKS / GKE / AKS / k3s cluster per customer — single-tenant, never shared.
Bootstrap order:
Provision the empty cluster under the customer's billing account (the
customer's cloud bill, not MeterBox's). Cluster name pattern:
bp-<customer-slug>-<region>.
Mirror the images from ghcr.io/<owner>/meterbox/{control-plane, data-plane}:<tag> into the customer's image registry (ECR, GAR, ACR).
GHCR images are private — pull secrets from the MeterBox ops keychain are
used for the mirror step only; the cluster pulls from the customer
registry afterward.
Install the helm chart with the Managed-tier values overlay:
helm install meterbox ./deploy/helm/meterbox \
--namespace meterbox --create-namespace \
-f deploy/helm/meterbox/values.yaml \
-f managed-tier-overlay.yaml
The overlay sets security.networkPolicies.enabled=true,
controlPlane.replicas=2, dataPlane.warmPool.enabled=true, and the
image-registry overrides.
Wire KMS for backup-mirror keys. Set the per-org backup target to
key_source: "kms" with the customer's KMS provider + key ARN (see
src/control-plane/services/backup-mirror.js).
The plaintext data-key never lives on disk; MeterBox only sees the
wrapped DEK.
Bootstrap an initial owner account via the standard signup flow,
transfer ownership to the customer's named operator, then have SRE step
down to admin role (or remove entirely once the customer has multiple
owners).
Hand over the customer-portal token-mint endpoint (POST /v1/customers/:id/tokens)
so the customer can wire their own end-customer JWT minting if they
plan to embed <CustomerPortal> (see
sdks/customer-portal/).
MeterBox SRE runs a 24×7 follow-the-sun rotation per Managed tenant — shared with general SRE for paging, but separate alert routing per cluster so one tenant's spike doesn't drown another's signal. Severities:
| Severity | Examples | Page target | SLO |
|---|---|---|---|
| Sev-1 | Data plane down, audit chain break, KMS decrypt failure | Page primary + secondary | 15 min ack, 1 hr engaged |
| Sev-2 | Single replica crashlooping, warm pool exhausted | Page primary | 30 min ack |
| Sev-3 | Slow query, alert backlog | Ticket | Next business day |
Runbooks per alert live in the MeterBox SRE wiki (out of scope for this document); links land in the alert payload itself.
image.tag field
is the only knob.--atomic is on by default; failed upgrades restore in-place.Encrypted off-cluster backups run nightly via the sweeper
(src/control-plane/services/backup-mirror-sweeper.js).
Managed-tier customers run with key_source: "kms", so each snapshot carries
a wrapped DEK + KMS key id on its manifest row. The plaintext DEK only exists
in process memory long enough to encrypt one tarball.
Quarterly restore drill (mandatory, evidence for SOC 2 + auditors):
Full procedure: compliance/dr-restore-drill.md.
Phases A–D (manifest → restore-token → cipher-byte download → decrypt) are
automated by npm run dr:drill, which prints a structured report you can
paste straight into the drill log template at
compliance/dr-drills/template.md.
Phases E–F (untar into a scratch PVC + bring up a read-only Postgres + verify
a ledger_entries row matches the live cluster) are the operator's portion
and run in a dr-drill-<yyyymmdd> namespace. Tear the namespace down after.
Commit the completed drill log under docs/compliance/dr-drills/<YYYY-Qn>.md
within 5 business days. The committed log is the SOC 2 evidence (CC7.5 +
A1.3).
If KMS Decrypt fails during a drill, treat as Sev-1 — the customer's KMS policy or MeterBox's IAM role has drifted and live restore would also fail.
The Managed tier has four key materials and three rotation cadences:
| Key | Holder | Cadence | How |
|---|---|---|---|
| Backup-mirror DEK | Customer KMS (per-snapshot ephemeral) | Per-snapshot (automatic) | Each snapshot generates a fresh DEK — no operator action |
| Backup-mirror master (KMS CMK) | Customer KMS | Annual (customer policy) | Customer rotates in their KMS; MeterBox picks up new ARN via setBackupTarget (no chart change) |
JWT_SECRET (customer-portal JWTs) |
In-cluster secret | Quarterly | kubectl rollout restart after secret update; customer-portal JWTs are 1h TTL so the window closes fast |
SESSION_COOKIE_SECRET + CP_JWT_PRIVATE_KEY/CP_JWT_PUBLIC_KEY |
In-cluster secret | Quarterly | Same — sliding 7-day idle ceiling means sessions naturally re-anchor |
ADMIN_API_KEY |
In-cluster secret | Quarterly OR after any suspected exposure | Rotate on rollout; customer's CI must update its env at the same time |
When METERBOX_RBAC_STRICT=true is on (Managed tier default — see § 1.6),
admin-key rotation can be staged: keep the matrix tight on destructive
actions during the rotation window so a leaked old key has the smallest
possible blast radius.
Managed-tier overlay sets these defaults that don't ship by default in Self-Managed:
# managed-tier-overlay.yaml
controlPlane:
env:
METERBOX_RBAC_STRICT: "true"
METERBOX_ORG_ID: "<customer-org-id>" # pinned at bootstrap
BACKUP_MIRROR_ENABLED: "true"
security:
networkPolicies:
enabled: true
podSecurityStandard: restricted
dataPlane:
warmPool:
enabled: true
minReady: 1
SRE access to the cluster is never via long-lived kubeconfig handed to laptops. The pattern (§ 3 for the network shape):
kubectl against the cluster API.No exceptions — even Sev-1 mid-incident sessions go through the same path. If the broker is unreachable, the page escalates to the customer's CISO line rather than bypassing it.
The control-plane audit log carries every privileged action — logins, role
changes, MFA events, backup-target mutations, RBAC-matrix edits, customer
archival, credit grants/revokes, GDPR exports — with actor, IP, user-agent,
and outcome. The chain is hash-evidenced
(docs/compliance/audit-trail-architecture.md)
so SIEM downstream can re-verify integrity if it wants.
| Shape | Endpoint | Use case |
|---|---|---|
| Pull (NDJSON window) | GET /cp/audit/export?since=&until= |
Ad-hoc SOC 2 evidence pulls; one-shot back-fills; auditor walk-through |
| Push (cron batch) | n/a (transport-side cron calling the pull endpoint) | Continuous SIEM ingestion |
There is no streaming WebSocket / SSE push from MeterBox. The cron pattern keeps SIEM transports decoupled from the control-plane process and means a SIEM outage doesn't backpressure billing operations.
GET /cp/audit/export?since=<iso>&until=<iso> returns application/x-ndjson,
oldest-first, one event per line. Owner/admin only. Window defaults to last
24h if no params supplied. Pagination is implicit via the window — page by
shifting since/until. See
src/control-plane/routes/auth.js for
the full handler.
Each line is one event:
{"id":"01HZ...","timestamp":"2026-06-02T15:34:17.881Z","actor":{"kind":"session","user_id":"u_1","ip":"10.0.0.7"},"event":"credit.granted","target":{"customer_id":"cust_42","grant_id":"cg_1"},"diff":{"amount_usd":[null,25]},"outcome":"success","chain":{"prev":"<sha256>","self":"<sha256>"}}
The recommended pattern is an in-cluster CronJob that pages the pull endpoint every 5 minutes with the prior window and POSTs each line to the customer's SIEM. Sample for Splunk HEC:
# audit-to-splunk.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: audit-to-splunk
namespace: meterbox
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: shipper
image: alpine/curl:8
env:
- name: BP_ADMIN_KEY
valueFrom: { secretKeyRef: { name: bp-admin, key: key } }
- name: SPLUNK_HEC_URL
value: https://splunk.customer.internal:8088/services/collector/raw
- name: SPLUNK_HEC_TOKEN
valueFrom: { secretKeyRef: { name: splunk-hec, key: token } }
command: ["/bin/sh", "-c"]
args:
- |
SINCE=$(date -u -d '6 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
UNTIL=$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)
curl -sS -H "x-admin-key: $BP_ADMIN_KEY" \
"http://control-plane.meterbox:3000/cp/audit/export?since=$SINCE&until=$UNTIL" \
| curl -sS -X POST "$SPLUNK_HEC_URL" \
-H "Authorization: Splunk $SPLUNK_HEC_TOKEN" \
--data-binary @-
The 1-minute trailing buffer accounts for clock skew between MeterBox and
the SIEM; the 6-to-1 window means each event ships exactly once under normal
operation and at-least-once on retry (Splunk dedups via id).
Same skeleton, different POST target:
# diff vs Splunk version — env + args only
env:
- name: DD_API_KEY
valueFrom: { secretKeyRef: { name: datadog, key: api-key } }
args:
- |
SINCE=...; UNTIL=...
curl -sS -H "x-admin-key: $BP_ADMIN_KEY" "$BP_EXPORT_URL?since=$SINCE&until=$UNTIL" \
| jq -c '{ddsource:"meterbox",service:"control-plane",message:.}' \
| curl -sS -X POST "https://http-intake.logs.datadoghq.com/api/v2/logs" \
-H "DD-API-KEY: $DD_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-
args:
- |
SINCE=...; UNTIL=...
KEY="audit/$(date -u +%Y/%m/%d/%H%M).ndjson"
curl -sS -H "x-admin-key: $BP_ADMIN_KEY" "$BP_EXPORT_URL?since=$SINCE&until=$UNTIL" \
| aws s3 cp - "s3://customer-bp-audit/$KEY"
Audit events are pruned in-cluster per the retention policy (configurable in SettingsPage → Security → Data retention). The default is 365 days, which is the SOC 2 minimum. The SIEM is the long-term system of record — operators should set the in-cluster retention to the shortest value that still covers the SIEM's ingest backfill window (typically 7–14 days), not the longest.
Once per quarter, run the SIEM-side count against the MeterBox-side count for the same 24h window. Any drift > 0.1% is investigated as a Sev-2.
The Managed tier runs in the customer's VPC. MeterBox has no inbound control-plane connectivity to the cluster except through the customer's network — there is no MeterBox-owned VPN, no MeterBox jumphost, no phone-home. Three deployment shapes:
The MeterBox SRE laptops sit in MeterBox's own ops VPC, which is peered to the customer's VPC via cross-account VPC peering (AWS), shared VPC (GCP), or VNet peering (Azure). Peering rules:
bp-ops-bastion security group can reach the
customer's bp-jumphost SG on port 22.For customers running a multi-account / multi-region landing zone, peering goes through the customer's Transit Gateway (AWS) or Cloud Router (GCP). The TGW route table allowlists the MeterBox ops VPC CIDR; the customer's security team owns the routes and can revoke any time.
No peering, no internet egress. SRE access is via the customer's own operator portal (Apple Remote Desktop / Citrix / customer-managed SSH gate). MeterBox provides the runbook + scripted upgrade flow; the customer's ops team runs the commands. This is the only path that satisfies certain defense / classified-network requirements.
In this shape, image upgrades flow through a sneakernet pipeline: MeterBox publishes the new image tag + SHA, customer pulls into their air-gap image registry, then runs the helm upgrade.
The control plane has a small allowlist of egress destinations, all configurable and all optional:
| Destination | Purpose | When |
|---|---|---|
| Stripe API + webhooks | Subscription mutations + webhook delivery | If the tenant uses Stripe billing |
| Resend (email) | Transactional email (signup, password reset, low-balance alerts) | If RESEND_API_KEY set |
| OIDC provider issuer | SSO token exchange | If SSO enabled |
| Customer KMS endpoint | Backup-mirror Decrypt | Always (Managed tier ships with KMS) |
| Customer SIEM intake | Audit shipping (§ 2) | Always |
There is no telemetry to MeterBox — no error reporting, no usage analytics, no version-check ping. Egress to MeterBox is impossible because no MeterBox endpoint is configured. This is the same posture as Self-Managed.
# from values.yaml — Managed-tier overlay enables both:
security:
networkPolicies:
enabled: true
The chart's network-policies.yaml defines:
kube-system/coredns only.A misconfigured ingress controller is the failure mode to watch — if a
customer's ingress doesn't match the labels in the chart's allowlist, the
control plane will fail liveness. Fix is to either relabel the ingress
controller or update the chart's security.ingressSelector.matchLabels.
Every kubectl session from the jumphost is recorded by the customer's auditd (Linux) or equivalent and streamed to the customer's SIEM by the customer's own pipeline. MeterBox does not see SRE shell-history; the customer does. This is deliberate: it puts the customer in control of operator attribution evidence, which auditors prefer.
The MeterBox-side audit (§ 2) covers everything that happened through the APIs. The customer-side jumphost audit covers everything that happened outside the APIs (kubectl, helm, image rollouts). Together they form the full picture; either alone is incomplete by design.
Single calendar for every recurring commitment the policy set makes. Owners keep the logs in the linked locations; the Security lead reviews the calendar at each quarterly review.
| Cadence | What | Owner | Where logged |
|---|---|---|---|
| Quarterly | DR restore drill (procedure) — npm run dr:drill automates phases A–D; operator runs E–F in a scratch namespace |
SRE + Security observer | docs/compliance/dr-drills/<YYYY-Qn>.md |
| Quarterly | IR tabletop (procedure) — facilitator picks a § 5 BCP scenario; participants role-play through it | Security lead | docs/compliance/tabletops/<YYYY-Qn>.md |
| Quarterly | Access review — GET /cp/access-review?format=csv per tenant + the internal cluster; flag + remove stale members within 14 days |
Security lead | Personnel file roll-up |
| Quarterly | Risk register review — re-rate each row (likelihood / impact / status), open + retire risks, note material changes | Security lead | risk-register.md quarterly review log |
| Quarterly | Vendor management spot-check — confirm subprocessor list still matches the public page; pick 1–2 vendors for an off-cycle review | Security lead | Vendor files |
| Annual | BCP tabletop (procedure § 10) — end-to-end exercise of one § 5 scenario with CEO + Security + SRE | Security lead | docs/compliance/bcp-tabletops/<YYYY>.md |
| Annual | Security training refresher (curriculum § 4) — every employee + contractor; ≤ 14 days for new hires | Security lead | Personnel file |
| Annual | External pentest (RoE) — full-scope; on material change in addition to the calendar | Security lead | Password manager (Restricted) + soc2 map row update |
| Annual | Bug-bounty program review (structure § 9) — SLA hit rate, tier benchmarking, phase graduation decision | Security lead | Year's pentest review notes |
| Annual | Policy review — every doc under docs/compliance/ gets a "Last reviewed" stamp + CEO sign-off where applicable |
Security lead + CEO | Each doc's frontmatter |
| Continuous | Backup-mirror sweep (nightly), reclaim sweeper (hourly), audit-retention sweep (daily) — automated; SRE reviews failures in the weekly sync | SRE | Sweeper logs + Sev-2 escalation when sustained |
| Continuous | Security-scan CI (npm audit + Trivy + SBOM) — every PR + push + weekly schedule |
Engineering lead | GitHub Security tab |
Missed cadence → open a Sev-2; sustained miss (two consecutive quarters) → Sev-1 and a CEO-signed exception per the InfoSec policy § 12. The cadence log IS the SOC 2 evidence for CC1.5 / CC4.1 / CC7.4 / CC7.5 / CC9.2 / A1.3.