Skip to content
academia.sh

Lesson 25 / 30

Security Context

How the cluster enforces its privilege and capability settings is measured: an admission-control rule is written and applied to the existing pod definitions; how many definitions violate policy, how many deployments the enforcement stops, the difference between warn mode and block mode, and how many definitions stay exposed once an exemption is granted are all counted.

Published Updated

In this lesson

  • Applies an admission-control rule to the existing pod definitions and counts violations per rule
  • Measures warn mode against block mode separately: stopped deployments and warning records
  • Counts how many definitions an exemption and a rule subset leave exposed relative to the full policy
Contents

The previous lesson’s policy bounded traffic between pods. It did not bound what a pod can do inside itself: a process that runs privileged, shares the host machine’s network, or is opened with a wide capability set opens up another domain on the node it sits on, even with a fully written network policy. What each of these settings does individually and what it costs on a single machine was measured earlier — capability counts, a read-only root, resource limits. That measurement is not repeated here.

This lesson’s question is different and narrower: how does the cluster enforce these settings. Even if a setting’s correct value is known, having that value present in all twenty-four separate definitions does not happen on its own. The delegated decision is this: an admission controller decides whether a pod definition gets accepted into the cluster by looking at its content. It needs one item, and a cheap one — the definition itself, which is already submitted to the cluster. This lesson measures not the cost of information but the outcome of the decision: what stops once enforcement is turned on.

  • OO25. Twenty-four pod definitions are spread across three namespaces and belong to the fictional regional measurement network. The definitions were built with a generator whose seed is visible; they were not read from a real repository. No real cluster is run.
  • OO26. Security context is reduced to six fields. What the fields mean and what they cost on a single machine were measured earlier; here only the count of which value appears in which definition is measured.
  • OO27. Admission control looks at what the definition declares. What the image actually does at runtime is outside this control’s field of view; this boundary is taken up separately at the end of the measurement.
  • OO28. The deployment flow is forty attempts, generated with a separate seed. The same definition can appear more than once in the flow; the forty attempts touch nineteen distinct definitions.
  • OO29. Exemption is granted in two forms: to an entire namespace, and to definitions marked “legacy.” Per-definition exemption is not modeled; had it been, the exposed count would drop, but its direction would not change.
  • OO30. No deployment stops in warn mode. A warning record is one line per violation, not per definition.

Definitions and Rules

// admission.mjs — pod definitions and admission control; not a real cluster, a model built with node
export const SEED = 20260805, NAMESPACE = ['development', 'staging', 'production'], DEFINITION_COUNT = 24;
export function generator(seed) {                        // linear congruential generator, seed visible
  let x = seed >>> 0;
  return () => { x = (Math.imul(x, 1664525) + 1013904223) >>> 0; return x / 4294967296; };
}
// Our own schema: every definition carries six security-context fields.
export function definitions() {
  const r = generator(SEED), t = [];
  for (let i = 0; i < DEFINITION_COUNT; i += 1) {
    const namespace = NAMESPACE[i % NAMESPACE.length];
    const legacy = r() < 0.45;                           // legacy definitions are written more loosely
    t.push({ name: `def-${String(i + 1).padStart(2, '0')}`, namespace, legacy,
      privileged: r() < (legacy ? 0.30 : 0.04),
      rootUser: r() < (legacy ? 0.80 : 0.35),
      readOnlyRoot: r() < (legacy ? 0.10 : 0.55),
      allowEscalation: r() < (legacy ? 0.85 : 0.40),
      hostNetwork: r() < (legacy ? 0.25 : 0.04),
      extraCapabilities: Math.floor(r() * (legacy ? 5 : 2)) });
  }
  return t;
}
export const RULE = [                                   // admission-control rules
  ['must not run privileged', (d) => !d.privileged],
  ['must not run as root', (d) => !d.rootUser],
  ['root filesystem must be read-only', (d) => d.readOnlyRoot],
  ['must not allow privilege escalation', (d) => !d.allowEscalation],
  ['must not use host network', (d) => !d.hostNetwork],
  ['must not request extra capabilities', (d) => d.extraCapabilities === 0],
];
export const violations = (d, rules = RULE) => rules.filter(([, f]) => !f(d)).map(([name]) => name);
export const compliant = (d, rules = RULE) => violations(d, rules).length === 0;

A rule is a predicate that looks at one field of a definition; policy is the list of these predicates. The definitions’ “legacy” flag is the model’s one bias: definitions written earlier were kept more likely to carry loose values. In a real repository, this bias is measured, not assumed.

Existing Definitions Checked Against the Rules

// run10.mjs — existing definitions are checked against the rules: how many fail, which rule how often
import { definitions, RULE, NAMESPACE, SEED, violations, compliant } from './admission.mjs';

const t = definitions();
console.log(`seed ${SEED} | ${t.length} pod definitions, ${NAMESPACE.length} namespaces, ${RULE.length} rules`);
console.log(`definitions that violate policy: ${t.filter((d) => !compliant(d)).length}/${t.length}\n`);
console.log('rule                                  violating definitions');
for (const [name, f] of RULE) console.log(`${name.padEnd(38)} ${t.filter((d) => !f(d)).length}`);
console.log('\nviolations  definitions');
const dist = new Map();
for (const d of t) dist.set(violations(d).length, (dist.get(violations(d).length) ?? 0) + 1);
for (const n of [...dist.keys()].sort((a, b) => a - b)) console.log(`${String(n).padEnd(12)} ${dist.get(n)}`);
console.log('\nnamespace    defs   failing  total violations');
for (const a of NAMESPACE) {
  const g = t.filter((d) => d.namespace === a);
  console.log(`${a.padEnd(12)} ${String(g.length).padEnd(6)} ${String(g.filter((d) => !compliant(d)).length).padEnd(8)} ` +
    `${g.reduce((s, d) => s + violations(d).length, 0)}`);
}
seed 20260805 | 24 pod definitions, 3 namespaces, 6 rules
definitions that violate policy: 19/24

rule                                  violating definitions
must not run privileged                2
must not run as root                   14
root filesystem must be read-only      11
must not allow privilege escalation    11
must not use host network              5
must not request extra capabilities    10

violations  definitions
0            5
1            6
2            2
3            5
4            3
5            2
6            1

namespace    defs   failing  total violations
development  8      6        19
staging      8      7        13
production   8      6        21

Nineteen of twenty-four definitions violate policy. This number alone says nothing; the interesting part is the second table. Of the nineteen non-compliant definitions, six carry a single violation, and one carries all six. So “a definition that violates policy” is not one class: at one end sit definitions a single-line fix would bring into compliance, at the other end one that needs to be rewritten from scratch. Total violations come to fifty-three, more than twice the number of definitions.

The rule table shows where enforcement will get harder. The rule that looks strictest — must not run privileged — hits only two definitions; the rule that hits the most, the ban on root user, hits fourteen. How “strict” a policy is gets measured not by how severe its rules are, but by how far they sit from existing definitions. The namespace table confirms this: the count of non-compliant definitions sits between six and seven across all three environments, but total violations are twenty-one in production, thirteen in staging. Production’s definitions are not fewer in number — they are non-compliant more deeply.

Warn, Block, Exemption

// run11.mjs — warn mode, block mode, and exemption are counted separately
import { definitions, RULE, generator, violations, compliant } from './admission.mjs';

const FLOW_SEED = 77021, DEPLOYMENTS = 40;
const t = definitions(), r = generator(FLOW_SEED);
const flow = Array.from({ length: DEPLOYMENTS }, () => t[Math.floor(r() * t.length)]);
console.log(`deployment flow: seed ${FLOW_SEED}, ${DEPLOYMENTS} attempts, ${new Set(flow.map((d) => d.name)).size} distinct definitions`);
console.log(`non-compliant attempts in the flow: ${flow.filter((d) => !compliant(d)).length}\n`);

const SCENARIO = [
  ['warn, no exemption', RULE, () => false, false],
  ['block, no exemption', RULE, () => false, true],
  ['block, development exempt', RULE, (d) => d.namespace === 'development', true],
  ['block, legacy definitions exempt', RULE, (d) => d.legacy, true],
  ['block, two rules only', RULE.slice(0, 1).concat([RULE[4]]), () => false, true],
];
console.log('scenario                        rules  stopped     passing         exposed under   warning');
console.log('                                       deployments non-compliant   full policy     records');
for (const [name, rules, exempt, block] of SCENARIO) {
  let stopped = 0, passingNonCompliant = 0, warnings = 0;
  for (const d of flow) {
    const i = violations(d, rules);
    if (i.length === 0) continue;
    if (exempt(d)) { passingNonCompliant += 1; warnings += i.length; continue; }
    if (block) stopped += 1; else { passingNonCompliant += 1; warnings += i.length; }
  }
  const exposed = t.filter((d) => !compliant(d) && (exempt(d) || !block || compliant(d, rules))).length;
  console.log(`${name.padEnd(32)} ${String(rules.length).padEnd(6)} ${String(stopped).padEnd(11)} ` +
    `${String(passingNonCompliant).padEnd(15)} ${String(exposed).padEnd(15)} ${warnings}`);
}
console.log('\nif the six rules were enforced one at a time instead of together, deployments stopped per rule:');
for (const k of RULE) {
  const d = flow.filter((x) => violations(x, [k]).length).length;
  console.log(`  ${k[0].padEnd(37)} ${d}`);
}
deployment flow: seed 77021, 40 attempts, 19 distinct definitions
non-compliant attempts in the flow: 33

scenario                        rules  stopped     passing         exposed under   warning
                                       deployments non-compliant   full policy     records
warn, no exemption               6      0           33              19              89
block, no exemption              6      33          0               0               0
block, development exempt        6      22          11              6               33
block, legacy definitions exempt 6      17          16              11              59
block, two rules only            2      6           0               13              0

if the six rules were enforced one at a time instead of together, deployments stopped per rule:
  must not run privileged               3
  must not run as root                  24
  root filesystem must be read-only     23
  must not allow privilege escalation   17
  must not use host network             5
  must not request extra capabilities   17

Same policy, same definitions, same flow; the only thing that changes is the enforcement mode. In warn mode, no deployment stopped, eighty-nine warning records were produced, and nineteen definitions stayed exposed. In block mode, thirty-three of forty attempts stopped — eighty-two percent. The distance between these two rows is the distance between writing a policy and enforcing it; switch from one to the other without letting a single day pass in between, and the cluster’s deployment flow effectively stops.

The exemption rows measure two ways of shortening that distance, and the two do not do the same thing. When the development namespace is exempted, stopped deployments drop from thirty-three to twenty-two, and six definitions stay exposed. When legacy definitions are exempted, stopped deployments drop as far as seventeen — the flow passes more comfortably — but exposed definitions rise to eleven and warning records to fifty-nine. What the measure says is: the more an exemption relaxes things, the more it leaves exposed, and the “legacy definitions” exemption exempts exactly the worst definitions, because most violations already sit in them.

The last row measures a third route: enforcing not the whole policy but two of its rules. Stopped deployments drop to six, passing non-compliant deployments to zero — nothing slips past those two rules. But the last column says thirteen definitions stay exposed relative to the full policy. This is a different kind of exposure from exemption: with exemption, the rule exists and is not applied to some definitions; with a rule subset, the rule does not exist at all, so no warning gets produced either. The warning column reading zero is the proof — none of the thirteen exposed definitions shows up anywhere.

The block below ties the choice of which two rules to a number. The ban on root user alone would have stopped twenty-four of the forty attempts, a read-only root twenty-three. The privilege ban would stop three, the host-network ban five. The rules to enforce first are chosen by looking at this column: the rules that stop the fewest deployments are the ones that can be closed with the least resistance, and once closed they do not reopen.

Progressive Enforcement

The numbers the block below lays out produce a program: rules can be turned on cumulatively, least-stopping first. At every step, stopped deployments, definitions that need fixing, and definitions still exposed are counted separately.

// run12.mjs — rules enforced cumulatively, least-stopping first
import { definitions, RULE, generator, violations, compliant } from './admission.mjs';

const t = definitions(), r = generator(77021);
const flow = Array.from({ length: 40 }, () => t[Math.floor(r() * t.length)]);
const order = [...RULE].sort((a, b) =>
  flow.filter((x) => !a[1](x)).length - flow.filter((x) => !b[1](x)).length);
console.log('step  rule added                          stopped     to fix        exposed under');
console.log('                                          deployments definitions   full policy');
for (let n = 1; n <= order.length; n += 1) {
  const subset = order.slice(0, n);
  const stopped = flow.filter((d) => violations(d, subset).length).length;
  const toFix = t.filter((d) => !compliant(d, subset)).length;
  const exposed = t.filter((d) => !compliant(d) && compliant(d, subset)).length;
  console.log(`${String(n).padEnd(5)} ${order[n - 1][0].padEnd(36)} ${String(stopped).padEnd(11)} ` +
    `${String(toFix).padEnd(13)} ${exposed}`);
}
step  rule added                          stopped     to fix        exposed under
                                          deployments definitions   full policy
1     must not run privileged              3           2             17
2     must not use host network            6           6             13
3     must not allow privilege escalation  20          13            6
4     must not request extra capabilities  30          17            2
5     root filesystem must be read-only    30          17            2
6     must not run as root                 33          19            0

The first two steps are cheap: six deployments stop, six definitions get fixed, and thirteen definitions stay exposed. The third step makes the jump — stopped deployments go from six to twenty — but in return, exposed definitions drop from thirteen to six. This shape of the curve shows that it is possible to stop somewhere: in three steps, about two-thirds of the full policy’s exposure closes while stopping exactly half the deployment flow.

The fourth and fifth rows carry the same three numbers. Nothing changes when the read-only-root rule is added: after step four, every definition that violates that rule already violates another rule too. The rule alone hit eleven definitions, but in the cumulative order it brings zero new definitions. A rule’s value is measured not by how many definitions it hits alone, but by what it adds on top of the others; an enforcement program written without knowing the order cannot see this difference.

What the Control Does Not See

This entire measurement was done over values the definitions declare. Admission control reads a pod definition, looks at six fields, and decides; the decision costs one read and one request’s worth of latency. The source of this cheapness is that declaration is the only thing the control can look at.

A declaration being correct does not mean the behavior is correct too. A definition’s image that declares it wants no extra capability might still attempt an operation at startup that requires one; the control cannot see this, because it runs before deployment, while the process does not yet exist. The reverse holds too: a definition that declares it runs as root user might do nothing with root at all. In the first case, the cluster assumes compliance and the process fails at runtime; in the second, the cluster counts it non-compliant and stops it needlessly. How many of the nineteen non-compliant definitions fall into the second class cannot be read from this model — reading it requires runtime observation, and that is another information item.

This is the delegated decision’s boundary. Admission control answers “does this definition comply with policy” with certainty; it does not answer “is this workload safe.” What closes the gap between the two is not one more rule but a production process that keeps declarations reflecting reality.

Summary

  • Nineteen of twenty-four definitions violate the six-rule policy, with fifty-three total violations; six non-compliant definitions carry one violation, one carries six, so non-compliance is not one class. Policy strictness also comes from distance to existing definitions rather than rule severity: the privilege ban hits two definitions, the root-user ban fourteen.
  • In warn mode, zero deployments stop and eighty-nine warnings get produced; in block mode, thirty-three of forty attempts stop, eighty-two percent. Switching between the two modes crosses that entire distance.
  • An exemption leaves exposed in proportion to how much it relaxes: the development-namespace exemption drops stopped deployments to twenty-two and leaves six definitions exposed; the legacy-definition exemption drops it to seventeen and leaves eleven exposed.
  • Enforcing a rule subset produces a different kind of exposure than exemption: with two rules, six deployments stop and thirteen definitions stay exposed relative to the full policy, but the warning count is zero — the exposure shows up nowhere.
  • When rules are turned on least-stopping first, exposure drops from nineteen to six in three steps; the rule added at the fifth step brings no new definitions, because its violations are already covered by the other rules.
  • Admission control looks at what the definition declares, not what the process does; it answers “does it comply with policy” with certainty and never answers “is it safe.”

Next Step

This lesson’s six fields had to be written separately into each of the twenty-four definitions, and nineteen of them were written wrong. The same holds for the previous three lessons’ outputs: the deployment pattern’s steps, the three probes’ periods and thresholds, role definitions, and network rules all sit in the same kind of definition files and get rewritten for every service. Repetition itself is a measure — how many files the same value appears in, how many files changing one value requires touching. The next lesson counts this repetition and measures what generating definitions from a single source removes, and what it adds.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close