---
title: 'Domain Policy Record'
source: 'https://academia.sh/en/courses/servers-and-hardening/domain-policy-record'
course: 'Servers, Networking and Hardening'
language: en
updated: '2026-09-13T05:08:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes which decision the policy declaration moves, and from whom to whom'
    - 'Compares the report-only, quarantine, and reject stages by their counts of dropped legitimate messages and blocked forged messages'
    - "Counts the report flow's rate of finding unauthorized senders and where that rate falls short"
---

# Domain Policy Record

Tightening the policy declaration for noncompliant messages in stages: the count of dropped legitimate messages and blocked forged messages at each stage, the report flow's rate of finding unauthorized senders, and the cost of rejecting outright.

The previous lesson ran both checks and produced two outcomes for every message. The
question left open there was what the outcome is for: when a message fails a check, does the
receiving side deliver it, separate it, or never accept it at all? By default, the name owner
does not make this decision. Each receiver acts on its own criteria; some deliver a failed
message as is, some apply their own estimate. The result is that the same forged message
reaches an inbox in one place and goes nowhere in another.

**The policy declaration** moves this decision back to the name owner. It is a record
published in the zone, and it says: apply this action to a message that arrives in this
name's name but fails the checks. It has three stages — **report only**, **quarantine**,
**reject** — and the difference between them is who gets billed for the gaps in the sending
infrastructure.

Jumping between stages is one of the most expensive countable mistakes in this topic. This
lesson runs one period and derives two numbers together at each stage: **how many legitimate
messages get dropped** and **how many forged messages get blocked**.

## The Fields the Declaration Carries

The record is written into the zone as text and declares four things: the action to apply to
a noncompliant message, what percentage of messages the action applies to, the address
reports get sent to, and whether a separate action is defined for subdomains. The first two
are the two axes this lesson measures; the third is the source of the measurement.

The declaration's scope is also a decision. The action applies only to messages that carry
this name in the **from field**; the check looks for the name that passed verification to
match the name in the from field. Without this requirement, the declaration would not work: a
message properly signed under its own name could carry a different name in the from field and
appear to pass the checks. The overlap requirement is the link that ties the surface the two
records close to the declaration's scope.

The reason a separate action can be defined for subdomains is that subdomains mostly do not
send messages. In the model's zone there are fourteen names, and only a few of them send; for
the subdomains that do not send, the strictest action can be declared directly, because there
is no legitimate message to lose there. This is the first and costless step of tightening,
and it stays outside the count below.

## One Period, Four Stages

**DR13.** The period is one month and consists of 414 fictional messages; flow counts are
taken as fixed.
**DR14.** The compliance criterion is that at least one of the two checks passes aligned with
the sender's name.
**DR15.** A legitimate message put into quarantine counts as delivered; the probability it
gets read is not counted in this model.

```javascript
// Model: one period's message flow and the policy declaration's stages. Flows,
// message counts, and the report rate are fictional; compliance means at
// least one of the two checks passes aligned with the sender's name.
const SEED = 7311;
const REPORT_RATE = 0.65;      // the rate at which receivers send reports

function prng(seed) {
  let x = seed % 2147483647;
  return () => { x = (x * 48271) % 2147483647; return (x - 1) / 2147483646; };
}

// sources: [address, message count]; the policy/signature fields are check results
const FLOWS = [
  { name: 'nightly-summary',        kind: 'legit',  policy: true,  signature: true,  sources: [['198.51.100.11', 120]] },
  { name: 'deviation-alert',        kind: 'legit',  policy: true,  signature: true,  sources: [['198.51.100.12', 60]] },
  { name: 'period-close',           kind: 'legit',  policy: true,  signature: false, sources: [['198.51.100.13', 40]] },
  { name: 'bulk-announcement',      kind: 'legit',  policy: true,  signature: true,  sources: [['203.0.113.40', 50], ['203.0.113.41', 40]] },
  { name: 'threshold-alert',        kind: 'legit',  policy: false, signature: false, sources: [['192.0.2.77', 25]] },
  { name: 'redirected-order',       kind: 'legit',  policy: false, signature: false, sources: [['198.51.100.30', 18]] },
  { name: 'billing-impersonation',  kind: 'forged', policy: false, signature: false, sources: [['192.0.2.200', 30], ['198.18.0.5', 2]] },
  { name: 'workorder-impersonation', kind: 'forged', policy: false, signature: false, sources: [['192.0.2.201', 15], ['198.18.0.9', 1], ['198.18.0.12', 1]] },
  { name: 'in-block-impersonation', kind: 'forged', policy: true,  signature: false, sources: [['203.0.113.200', 12]] },
];

const compliant = (a) => a.policy || a.signature;
const count = (a) => a.sources.reduce((t, [, n]) => t + n, 0);
const sum = (predicate) => FLOWS.filter(predicate).reduce((t, a) => t + count(a), 0);

const legit = sum((a) => a.kind === 'legit');
const forged = sum((a) => a.kind === 'forged');
console.log('period total:', legit + forged, '| legit:', legit, '| forged:', forged);
console.log('noncompliant legit messages:', sum((a) => a.kind === 'legit' && !compliant(a)),
  '| compliant forged messages:', sum((a) => a.kind === 'forged' && compliant(a)));

console.log('unprepared legit flows:',
  FLOWS.filter((a) => a.kind === 'legit' && !compliant(a)).map((a) => `${a.name}=${count(a)}`).join(' '));

const STAGES = ['no declaration', 'report only', 'quarantine', 'reject'];
console.log('\nstage           inbox (legit)         separated legit  inbox (forged)        blocked forged');
for (const stage of STAGES) {
  const separates = stage === 'quarantine' || stage === 'reject';
  const legitSeparated = separates ? sum((a) => a.kind === 'legit' && !compliant(a)) : 0;
  const forgedSeparated = separates ? sum((a) => a.kind === 'forged' && !compliant(a)) : 0;
  console.log(stage.padEnd(16) + String(legit - legitSeparated).padEnd(22) +
    `${legitSeparated}${stage === 'reject' ? ' (lost)' : stage === 'quarantine' ? ' (quarantine)' : ''}`.padEnd(17) +
    String(forged - forgedSeparated).padEnd(22) + String(forgedSeparated));
}

// Report flow: each message independently enters the report with REPORT_RATE odds.
// A source counts as found if at least one of its messages was reported.
const rand = prng(SEED);
const unauthorized = [];
for (const a of FLOWS) {
  if (a.policy) continue;                       // already in the policy record
  for (const [address, n] of a.sources) {
    let reported = 0;
    for (let i = 0; i < n; i++) if (rand() < REPORT_RATE) reported++;
    unauthorized.push({ address, flow: a.name, kind: a.kind, n, found: reported > 0 });
  }
}
const detected = unauthorized.filter((k) => k.found);
console.log('\nreport rate:', REPORT_RATE, '| seed:', SEED);
console.log('unauthorized sources:', unauthorized.length, '| found via reports:', detected.length,
  `(${Math.round((detected.length / unauthorized.length) * 100)}%)`);
for (const k of unauthorized.filter((x) => !x.found)) {
  console.log(`  missed source: ${k.address.padEnd(15)} ${k.flow.padEnd(18)} messages=${k.n}`);
}
console.log('message share of found sources:',
  `${detected.reduce((t, k) => t + k.n, 0)}/${unauthorized.reduce((t, k) => t + k.n, 0)}`);

// Prep: the new service is added to the policy record, the batch job is signed.
FLOWS.find((a) => a.name === 'threshold-alert').policy = true;
console.log('\nlegit messages lost in the reject stage after prep:',
  sum((a) => a.kind === 'legit' && !compliant(a)));
```

```
period total: 414 | legit: 353 | forged: 61
noncompliant legit messages: 43 | compliant forged messages: 12
unprepared legit flows: threshold-alert=25 redirected-order=18

stage           inbox (legit)         separated legit  inbox (forged)        blocked forged
no declaration  353                   0                61                    0
report only     353                   0                61                    0
quarantine      310                   43 (quarantine)  12                    49
reject          310                   43 (lost)        12                    49

report rate: 0.65 | seed: 7311
unauthorized sources: 7 | found via reports: 6 (86%)
  missed source: 198.18.0.12     workorder-impersonation messages=1
message share of found sources: 91/92

legit messages lost in the reject stage after prep: 18
```

## Reading the Stages

The first two rows carry the same numbers, and that is not a mistake. The **report-only**
stage blocks no message at all; sixty-one forged messages keep reaching the inbox. The only
thing this stage produces is a report, and a report is not a defense but a **measurement
tool**.

Tightening begins at the **quarantine** row: forty-nine forged messages are separated from
the inbox, and in exchange forty-three legitimate messages are separated too. The ratio is
almost one to one — meaning that alongside the closed open item, the wrong closure is of the
same magnitude. The only difference is that a quarantined legitimate message still sits
somewhere; the recipient finds it if they look.

**Reject** gives the same numbers as quarantine, but the column's meaning changes: the
forty-three legitimate messages are now nowhere at all. Two stages at the same strictness
diverge on the legitimate-message side in terms of **reversibility**. This is why the order
matters: quarantine is a rehearsal for rejecting, and it keeps the loss recoverable.

The last column shows the item that never closes. Twelve forged messages reach the inbox at
every stage, because they come from inside the network block authorized in the policy record
and count as compliant. The policy declaration says what to do with a noncompliant message; it
does not see a forgery that looks compliant. The overly broad authorization from the previous
lesson comes back here directly as an item: tightening the declaration without narrowing the
block does not touch these twelve messages.

## The Report's Value and Its Limit

The report flow is receivers telling the name owner: a message came from this source in this
name's name, and the check resulted in this. Its value is that the drift visible only in the
previous lesson's run becomes visible in production.

**DR16.** A portion of receivers send reports; each message independently has a 0.65
probability of entering a report.
**DR17.** A report shows the source, not whether the message was legitimate or forged.

The number is good: six of seven unauthorized sources are found, the find rate is **86%**,
and the found sources cover **91** of the messages. The one source that got away sent only a
single message during the period. The rule here is that the report flow finds a high-volume
source almost for certain and misses a sparse one. That means it is the right tool for
preparation — legitimate flows that would be dropped unprepared are, by definition, regular
and high-volume.

The limit is in DR17. The report says 25 noncompliant messages came from `192.0.2.77`; it
does not say whether that is a newly set-up threshold-alert service or an impersonation. What
makes that distinction is not the report but the **inventory** — comparison against the list
of services that actually send. The report flow produces the question, not the answer.

## The Cost of Rejecting, and Preparation

The cost of moving straight to rejecting is written by name in the output: threshold-alert,
25 messages; redirected-order, 18 messages. The first is a new service waiting to be added to
the record; its loss is a preparation gap. The second is a redirect field teams set up to
their own addresses, and it is not under the name owner's control.

The preparation step turns this distinction into a number: after the new service is added to
the policy record, the legitimate messages lost in the reject stage drop from **43 to 18**.
The remaining 18 messages, however, cannot be closed with a record — the redirect makes the
message come from a different machine and changes its body.

How long preparation needs to run also comes from the report. A single-period report shows
only the sources that sent during that period; a close-out job that runs at the start of the
quarter, or a notification sent once a year, never shows up in a one-period window. The one
source that got away having sent a single message during the period is the small-scale
example of this. The preparation window must cover the period of the sparsest sending flow;
kept short, tightening drops that flow the first day it runs.

This is the justification for staged tightening. In the report stage, sources are found,
records are completed, and the magnitude of the remaining loss is measured; only after that
does the move go to quarantine, then to rejecting. If the order is skipped, the loss ends up
the same size, but it is experienced first and learned about afterward.

## Tightening by Percentage

A stage is not a single switch. The declaration can also say the action applies to only a
percentage of messages; tightening is thereby graduated within a stage too.

```javascript
// Model: the declaration can apply to only a percentage of noncompliant
// messages. Input is the prior counts: two unprepared legit flows, 49 forged.
const SEED = 9013;
const LEGIT_FLOWS = [['threshold-alert', 25], ['redirected-order', 18]];
const NONCOMPLIANT_FORGED = 49;
const NOTICE_THRESHOLD = 10;   // this many losses in a flow per period produce a complaint

function prng(seed) {
  let x = seed % 2147483647;
  return () => { x = (x * 48271) % 2147483647; return (x - 1) / 2147483646; };
}

const countHits = (rand, n, rate) => {
  let k = 0;
  for (let i = 0; i < n; i++) if (rand() < rate) k++;
  return k;
};

console.log('seed:', SEED, '| notice threshold:', NOTICE_THRESHOLD, 'messages/period');
console.log('rate   threshold-alert  redirected-order    total legit lost    forged blocked    flows past threshold');
for (const rate of [0.05, 0.1, 0.25, 0.5, 1.0]) {
  const rand = prng(SEED);
  const losses = LEGIT_FLOWS.map(([, n]) => countHits(rand, n, rate));
  const forgedBlocked = countHits(rand, NONCOMPLIANT_FORGED, rate);
  console.log(
    `${Math.round(rate * 100)}%`.padEnd(7) +
    String(losses[0]).padEnd(17) + String(losses[1]).padEnd(20) +
    String(losses[0] + losses[1]).padEnd(20) + String(forgedBlocked).padEnd(18) +
    String(losses.filter((k) => k >= NOTICE_THRESHOLD).length),
  );
}
```

```
seed: 9013 | notice threshold: 10 messages/period
rate   threshold-alert  redirected-order    total legit lost    forged blocked    flows past threshold
5%     0                2                   2                   2                 0
10%    4                3                   7                   4                 0
25%    9                6                   15                  12                0
50%    14               12                  26                  26                2
100%   25               18                  43                  49                2
```

Two things read at once in the table. First, a low percentage blocks forged messages at a low
rate too; at 10%, only four of the forty-nine forgeries get stopped. The percentage grades
**risk**, not defense.

Second, and the actually useful one, is the last column. Up to 25%, no flow crosses the
notice threshold; there is loss, but no one complains. This shows the percentage's real
function: a low rate is not a defense setting but a tool for **finding preparation gaps
cheaply**. A loss of fifteen messages shows up in reports and not with the field team; the
same flow, opened to 100%, loses twenty-five messages at once and work stops. The cost of
running tightening by percentage is that forged messages also get through at the same rate
every period that passes.

## The Remaining Item and the New Surface

**DR18.** In this model, the declaration is applied identically by every receiver.

This assumption overreaches reality and shows the part of this topic that **cannot be
measured**: the declaration is a request, not something enforceable. The receiving side may
not apply it, may apply it partially, or may layer its own criteria on top. Here the name
owner only declares a decision; someone else applies it, and whether it gets applied is
visible, incompletely, only through the report flow.

The new surface closing brings is the report flow itself. An address is written into the
zone, and continuous data arrives at that address from outside parties: how many messages
came from which source, which check failed. This is a flow that makes the measurement
network's sending structure readable from outside, and the address it goes to is written
openly in the zone for anyone. It is also a volume surface: the report address is a data
intake that comes from outside and must be processed.

## Summary

- The policy declaration moves the decision of what to do with a noncompliant message from
  the receiver to the name owner; without a declaration, the same forged message reaches an
  inbox at one receiver and goes nowhere at another.
- The report-only stage blocks no message: 61 forged messages keep getting through; this
  stage is a measurement tool, not a defense.
- Quarantine and reject give the same numbers (49 forged blocked, 43 legitimate separated);
  the difference is in the reversibility of the legitimate message.
- 12 forged messages coming from inside the authorized network block reach the inbox at every
  stage; the declaration separates the noncompliant but does not see a forgery that looks
  compliant.
- The report flow finds six of seven unauthorized sources (86%) and covers 91 of the
  messages; the source it misses sent a single message during the period.
- Preparation brings the legitimate loss in the reject stage down from 43 to 18; the
  remaining 18 messages come from a redirect and cannot be closed with any record.

## Next Step

The name now also defines who is permitted to send messages in its own name, and declares
what happens to a noncompliant message. All of this is a measure of the sending side. A
message can fail to arrive even after passing every check: the receiving side also decides
based on the sender's history — how many of its messages bounced, how many people complained,
whether the sending volume spiked suddenly. The next lesson models deliverability as a ratio,
counts the factors that degrade it separately, and runs one period: how many messages get
delayed when reputation drops, how many never arrive at all, and how many periods recovery
takes.
