Everything on the previous pages runs inside your process. The managed
platform at prod-orm.io sits outside it — at the data plane every client
authenticates through, with the credentials, the backups and the region under
its control — and can therefore do things a library cannot: notice that a
credential is being guessed at from one address and rotate it, revoke a key
that has been used from a country it never was before, pause a database that
has left its jurisdiction.
Those responses are policies: 150 of them, each a sentence of the form
when this is observed, do that. You switch them on per database, tune the
numbers, and the platform enforces them while you sleep. This page is about
what they are, how they fit the checklists, and the one question they were
built to answer — who logged in?
{
"id": "auth.failed.standard.rotate",
"name": "Failed logins (standard): rotate the credentials",
"category": "authentication",
"severity": "critical",
"description": "10 refused credentials within 5 minutes. The engine credentials are rotated — every client holding the old ones stops working until it is given the new ones — and the owner is told. …",
"trigger": { "kind": "threshold", "on": "auth.failed", "count": 10, "withinMinutes": 5 },
"actions": ["rotate-credentials", "notify-owner"],
"params": {
"count": { "type": "integer", "default": 10, "min": 1, "max": 100000, "description": "How many occurrences before the policy fires" },
"withinMinutes": { "type": "integer", "default": 5, "min": 1, "max": 43200, "description": "The window, in minutes" }
},
"cooldownMinutes": 5,
"frameworks": ["SOC 2 CC6.1", "NIST 800-53 AC-7"]
}
The vocabulary is closed on both ends, on purpose. A policy can only name a
signal the platform actually emits and an action it actually implements, so
every one of the 150 is a control, not a promise.
Triggers — six kinds:
| Kind | Fires when | Evaluated |
|---|---|---|
threshold | N events of a type within W minutes — grouped by address for the block-ip family, by key for the revoke-key family | as each event arrives |
event | every occurrence of an event | as it arrives |
metric | a reading crosses a line: usedPercent, cpu, memory, connections, requestsPerMinute, errorRate, monthlyCents | by the sweep |
age | something is older than N days: the last backup, the credentials, the oldest key, the last use, the last owner sign-in, the last policy review | by the sweep |
every | a schedule, in days | by the sweep |
state | a fact about the database now: no ip allowlist, a key without expiry, the owner has no MFA, outside its region… | by the sweep |
Actions — fifteen, each doing only what a signed-in owner could do by
hand: rotate-credentials, revoke-key, revoke-all-keys,
revoke-idle-keys, expire-keys, block-ip, pause, resume, backup,
restrict-readonly, lock-provisioning, revoke-owner-sessions,
notify-owner, notify-admins, record. Actions run in order; each
outcome is recorded whether or not the previous one succeeded; notifications
come last so they can say what was done.
Cooldown stops a sustained condition rotating credentials every second.
Params are the knobs an assignment may turn — nothing else is accepted.
sequenceDiagram
participant C as client with a guessed password
participant DP as data plane
participant P as policy engine
participant O as owner
C->>DP: authenticate (refused)
DP->>P: auth.failed recorded in the events feed
P->>P: every assigned policy on auth.failed: merge params over defaults
P->>P: threshold: count auth.failed in the last withinMinutes
alt below count, or inside the cooldown
P-->>DP: nothing
else threshold met
P->>DP: rotate-credentials (queued as an operation)
P->>O: notify-owner, naming what was done
P->>DP: policy.fired with the reason and every outcome
end
There is no retry. Each action is idempotent (revoke, block, downgrade) or
queued with an idempotency key, and the next event or sweep evaluates the
policy again.
Switching on the policy above with a tighter threshold, three ways:
$ prorm policies show auth.failed.standard.rotate
$ prorm policies assign auth.failed.standard.rotate --db billing --param count=4,withinMinutes=5
✓ Failed logins (standard): rotate the credentials is on for billing: 4 × auth.failed in 5m → rotate-credentials, notify-owner
$ prorm policies assigned --db billing
--pause keeps an assignment without evaluating it; prorm policies unassign <id> switches it off. prorm policies firings reads the
policy.fired events back.
$ curl -X PUT https://api.prod-orm.io/provision/databases/$DB_ID/policies/auth.failed.standard.rotate \
-H "authorization: Bearer $PRORM_TOKEN" -H 'content-type: application/json' \
-d '{ "params": { "count": 4, "withinMinutes": 5 }, "enabled": true }'
The answer is { "assignment": { … } }. A param the catalog does not
declare, or one outside its range, is a 400: colour is not a parameter,
count must be at least 1.
Open the database, then Policies. Find the policy in The catalog
(search takes soc 2, gdpr, backup), press Switch on, then set
count and withinMinutes on the assigned row and Save. Mark reviewed and
Check now sit above the list.
The routes in full:
GET /policies the catalog, grouped; ?category= ?severity= ?framework=soc%202 ?q=
GET /policies/{policyId}
GET /provision/databases/{id}/policies what is on, and what is not
PUT /provision/databases/{id}/policies/{policyId} { "params": { "count": 4 }, "enabled": true }
DELETE /provision/databases/{id}/policies/{policyId}
POST /provision/databases/{id}/policies/review "I have looked at these" — the quarterly-review clock
POST /provision/databases/{id}/policies/check evaluate the periodic ones now
GET /provision/databases/{id}/policies/trace?credential=prk_… who is behind a credential
Every firing is written to the database's events feed as policy.fired, with
the reason and every action's outcome — the evidence, in the place the rest
of the evidence already is.
Every api key is prefix.secret. The prefix is the watermark: it is what
appears in the engine's logs, in every auth.* event, in a leaked config
file — the half that is safe to write down. Given a whole key or just its
prefix, the trace names the person:
{
"prefix": "prk_3f9a1c2e",
"key": { "name": "billing-worker", "privilege": "readwrite", "createdAt": "…", "lastUsedAt": "…", "revokedAt": null, "live": true },
"issuedTo": { "username": "ada", "email": "ada@example.com" },
"addresses": [
{ "ip": "203.0.113.9", "first": "…", "last": "…", "count": 1412 },
{ "ip": "198.51.100.4", "first": "…", "last": "…", "count": 3 }
],
"recent": [ /* the last fifty auth and query events for this key */ ]
}
That is the answer to "an authenticated device had access — who was it?":
the account that was issued the key, and every address it has been used
from. A prefix that belongs to no key on that database is a 404, not a lead;
only the prefix is used and only the prefix is echoed back.
| Policy | When | Does |
|---|---|---|
auth.failed.strict.notify | 5 × auth.failed in 5 min | notify-owner, record |
auth.failed.strict.block-ip | 5 × auth.failed in 5 min | block-ip, notify-owner |
auth.failed.strict.rotate | 5 × auth.failed in 5 min | rotate-credentials, notify-owner |
auth.failed.strict.pause | 5 × auth.failed in 5 min | pause, notify-owner, notify-admins |
auth.failed.standard.notify | 10 × auth.failed in 5 min | notify-owner, record |
auth.failed.standard.block-ip | 10 × auth.failed in 5 min | block-ip, notify-owner |
auth.failed.standard.rotate | 10 × auth.failed in 5 min | rotate-credentials, notify-owner |
auth.failed.standard.pause | 10 × auth.failed in 5 min | pause, notify-owner, notify-admins |
auth.failed.lenient.notify | 25 × auth.failed in 15 min | notify-owner, record |
auth.failed.lenient.block-ip | 25 × auth.failed in 15 min | block-ip, notify-owner |
auth.failed.lenient.rotate | 25 × auth.failed in 15 min | rotate-credentials, notify-owner |
auth.failed.lenient.pause | 25 × auth.failed in 15 min | pause, notify-owner, notify-admins |
auth.failed.high-volume.notify | 100 × auth.failed in 60 min | notify-owner, record |
auth.failed.high-volume.block-ip | 100 × auth.failed in 60 min | block-ip, notify-owner |
auth.failed.high-volume.rotate | 100 × auth.failed in 60 min | rotate-credentials, notify-owner |
auth.failed.high-volume.pause | 100 × auth.failed in 60 min | pause, notify-owner, notify-admins |
auth.new-ip.notify | every auth.new-ip | notify-owner |
auth.new-country.notify | every auth.new-country | notify-owner |
auth.new-country.revoke | every auth.new-country | revoke-key, notify-owner |
auth.revoked-key.presented | 3 × auth.revoked-key in 60 min | block-ip, notify-owner |
auth.expired-key.presented | 5 × auth.expired-key in 1440 min | notify-owner |
auth.readonly-write.repeated | 10 × auth.readonly-write in 60 min | notify-owner |
auth.readonly-write.revoke | 25 × auth.readonly-write in 60 min | revoke-key, notify-owner |
auth.owner-signin-failed | 10 × owner.signin-failed in 15 min | revoke-owner-sessions, restrict-readonly, notify-owner |
auth.succeeded.new-key-first-use | every auth.succeeded | notify-owner |
auth.succeeded.burst | 1000 × auth.succeeded in 5 min | notify-owner |
auth.failed.spray | 20 × auth.failed in 10 min | block-ip, notify-owner |
| Policy | When | Does |
|---|---|---|
credentials.rotate.30d | every 30 d | rotate-credentials, notify-owner |
credentials.rotate.60d | every 60 d | rotate-credentials, notify-owner |
credentials.rotate.90d | every 90 d | rotate-credentials, notify-owner |
credentials.rotate.180d | every 180 d | rotate-credentials, notify-owner |
credentials.key-age.30d | oldest-active-key > 30 d | revoke-idle-keys, notify-owner |
credentials.key-age.60d | oldest-active-key > 60 d | revoke-idle-keys, notify-owner |
credentials.key-age.90d | oldest-active-key > 90 d | revoke-idle-keys, notify-owner |
credentials.key-age.180d | oldest-active-key > 180 d | revoke-idle-keys, notify-owner |
credentials.key-age.365d | oldest-active-key > 365 d | revoke-idle-keys, notify-owner |
credentials.idle-key.14d | last-use > 14 d | revoke-idle-keys, notify-owner |
credentials.idle-key.30d | last-use > 30 d | revoke-idle-keys, notify-owner |
credentials.idle-key.60d | last-use > 60 d | revoke-idle-keys, notify-owner |
credentials.idle-key.90d | last-use > 90 d | revoke-idle-keys, notify-owner |
credentials.key-without-expiry | state: key-without-expiry | expire-keys, notify-owner |
credentials.owner-key-exists | state: has-owner-key | restrict-readonly, notify-owner |
credentials.key-created.notify | every key.created | notify-owner |
credentials.role-privileged.notify | every role.privileged | notify-owner |
credentials.key-churn | 10 × key.created in 60 min | notify-owner |
| Policy | When | Does |
|---|---|---|
access.require-ip-allowlist | state: no-ip-allowlist | notify-owner |
access.require-verify-full | state: ssl-not-verify-full | notify-owner |
access.allowlist-cleared.notify | every access.ip-allowlist-cleared | notify-owner, notify-admins |
access.allowlist-cleared.revert | every access.ip-allowlist-cleared | pause, notify-owner, notify-admins |
access.changed.notify | every access.changed | notify-owner |
access.role-created.notify | every role.created | notify-owner |
access.role-churn | 5 × role.created in 60 min | notify-owner |
access.bulk-read.bulk.notify | 1 × query.bulk-read in 1 min | notify-owner |
access.bulk-read.very-large.notify | 1 × query.bulk-read in 1 min | notify-owner |
access.bulk-read.exfiltration-scale.notify | 1 × query.bulk-read in 1 min | notify-owner |
access.bulk-read.exfiltration-scale.revoke | 1 × query.bulk-read in 1 min | revoke-key, notify-owner, notify-admins |
access.bulk-read.repeated | 5 × query.bulk-read in 60 min | restrict-readonly, notify-owner |
access.blocked-queries.notify | 10 × query.blocked in 15 min | notify-owner |
access.blocked-queries.revoke | 50 × query.blocked in 60 min | revoke-key, notify-owner |
| Policy | When | Does |
|---|---|---|
data.storage.70pct.notify | usedPercent >= 70 | notify-owner |
data.storage.80pct.notify | usedPercent >= 80 | notify-owner |
data.storage.90pct.notify | usedPercent >= 90 | notify-owner |
data.storage.95pct.notify | usedPercent >= 95 | notify-owner |
data.storage.full.backup | usedPercent >= 95 | backup, notify-owner |
data.storage.over.pause | usedPercent >= 120 | backup, pause, notify-owner |
data.backup-age.1d | last-backup > 1 d | backup, notify-owner |
data.backup-age.7d | last-backup > 7 d | backup, notify-owner |
data.backup-age.30d | last-backup > 30 d | backup, notify-owner |
data.no-backup-ever | state: no-backup-ever | backup, notify-owner |
data.dedicated-without-backup | state: dedicated-without-backup | backup, notify-owner |
data.restore-test.90d | last-successful-restore-test > 90 d | notify-owner |
data.backup-failed.notify | every backup.failed | backup, notify-owner |
data.backup-failed.repeated | 3 × backup.failed in 1440 min | notify-owner, notify-admins |
data.restore-failed.notify | every restore.failed | notify-owner, notify-admins |
data.replica-lag.notify | every replica.lagging | notify-owner |
data.moved.notify | every resource.moved | notify-owner, notify-admins |
| Policy | When | Does |
|---|---|---|
availability.cpu.80pct | cpu >= 80 | notify-owner |
availability.memory.80pct | memory >= 80 | notify-owner |
availability.cpu.90pct | cpu >= 90 | notify-owner |
availability.memory.90pct | memory >= 90 | notify-owner |
availability.cpu.95pct | cpu >= 95 | notify-owner |
availability.memory.95pct | memory >= 95 | notify-owner |
availability.connections.50 | connections >= 50 | notify-owner |
availability.connections.100 | connections >= 100 | notify-owner |
availability.connections.200 | connections >= 200 | notify-owner |
availability.error-rate.5pct | errorRate >= 5 | notify-owner |
availability.error-rate.25pct | errorRate >= 25 | notify-owner, notify-admins |
availability.slow-queries | 20 × query.slow in 15 min | notify-owner |
availability.paused.notify | every resource.paused | notify-owner |
availability.resumed.notify | every resource.resumed | notify-owner |
availability.requests.10k | requestsPerMinute >= 10000 | notify-owner |
availability.requests.zero | requestsPerMinute <= 0 | notify-owner |
availability.storage-mb.1024 | usedMb >= 1024 | notify-owner, record |
| Policy | When | Does |
|---|---|---|
operations.idle.30d.notify | last-use > 30 d | notify-owner |
operations.idle.60d.notify | last-use > 60 d | notify-owner |
operations.idle.90d.notify | last-use > 90 d | notify-owner |
operations.idle.180d.notify | last-use > 180 d | notify-owner |
operations.idle.60d.pause | last-use > 60 d | backup, pause, notify-owner |
operations.idle.90d.pause | last-use > 90 d | backup, pause, notify-owner |
operations.idle.180d.pause | last-use > 180 d | backup, pause, notify-owner |
operations.failed.notify | every operation.failed | notify-owner |
operations.failed.repeated | 3 × operation.failed in 1440 min | notify-owner, notify-admins |
operations.owner-absent.90d | last-owner-signin > 90 d | notify-owner, notify-admins |
operations.owner-absent.365d.readonly | last-owner-signin > 365 d | restrict-readonly, notify-owner, notify-admins |
operations.require-replica | state: no-replica | notify-owner |
compliance.rotation.backup-after | every key.rotated | backup |
| Policy | When | Does |
|---|---|---|
billing.monthly.50 | monthlyCents >= 5000 | notify-owner |
billing.monthly.100 | monthlyCents >= 10000 | notify-owner |
billing.monthly.250 | monthlyCents >= 25000 | notify-owner |
billing.monthly.500 | monthlyCents >= 50000 | notify-owner |
billing.payment-failed.readonly | every billing.payment-failed | restrict-readonly, notify-owner |
| Policy | When | Does |
|---|---|---|
change.schema.notify | every schema.changed | notify-owner |
change.schema.backup-first | every schema.changed | backup |
change.schema.burst | 10 × schema.changed in 15 min | notify-owner |
change.migration.applied.notify | every migration.applied | notify-owner |
change.migration.failed.notify | every migration.failed | notify-owner, notify-admins |
change.resized.notify | every resource.resized | notify-owner |
change.rotated.notify | every key.rotated | notify-owner |
change.role-rotated.notify | every role.rotated | notify-owner |
change.stale-schema.180d | last-schema-change > 180 d | record |
change.policy-review.90d | last-policy-review > 90 d | notify-owner |
change.operation-cancelled.notify | every operation.cancelled | notify-owner |
change.policy-fired.admins | every policy.fired | notify-admins |
| Policy | When | Does |
|---|---|---|
compliance.owner-without-mfa | state: owner-without-mfa | notify-owner |
compliance.mfa-removed.readonly | every owner.mfa-removed | revoke-owner-sessions, restrict-readonly, notify-owner, notify-admins |
compliance.password-changed.notify | every owner.password-changed | notify-owner |
compliance.residency.eu | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.us | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.uk | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.ca | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.au | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.br | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.in | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.jp | state: region-outside | pause, notify-owner, notify-admins |
compliance.residency.sg | state: region-outside | pause, notify-owner, notify-admins |
compliance.shared-tier-forbidden | state: shared-tier | notify-owner |
compliance.owner-is-child | state: owner-is-child | record |
compliance.breach-clock.72h | last-policy-review > 3 d | notify-owner, notify-admins |
compliance.retention-purge.7d | every 7 d | record, notify-owner |
compliance.access-review.30d | every 30 d | notify-owner |
compliance.access-review.90d | every 90 d | notify-owner |
compliance.sheet.30d | every 30 d | notify-owner |
compliance.policies-never-reviewed | state: no-policies-reviewed | notify-owner |
compliance.restore.notify | every restore.completed | notify-owner, notify-admins |
compliance.credentials-check.1d | every 1 d | record |
| Policy | When | Does |
|---|---|---|
compliance.child-write.notify | every child.write-attempt | notify-owner |
compliance.child-write.repeated | 10 × child.write-attempt in 1440 min | notify-owner |
family.child-owner-no-ip-allowlist | state: no-ip-allowlist | notify-owner |
family.child-key-created.notify | every key.created | notify-owner |
family.child-idle.30d | last-use > 30 d | notify-owner |
The platform carries the parental side of the age gate
end to end, because a library cannot email a parent:
And the family.* policies — a parent told when a child's account is
refused a restricted action, creates a key, or leaves a database idle.
Switch on three, and tune from there:
| Policy | Why first |
|---|---|
auth.failed.standard.rotate | the control the whole feature was built for |
auth.new-country.notify | the cheapest signal that a key has travelled without its owner |
data.backup-age.1d | because everything else assumes there is a backup |
Then the pair every auditor asks about: compliance.access-review.30d and
change.policy-review.90d.
[!WARNING]
Arotate-credentialsaction stops every client holding the old engine
credentials until it is given the new ones, the attacker's and your own
workers alike. Before switching on a*.rotateor*.pausepolicy, make
sure your deploys can pick up new credentials without a person, or start
with the*.notifyvariant of the same threshold. The
credential rotation runbook covers
the application side.
Next: Status — what is production-grade, and what is not.