---
title: 'Infrastructure as Code Security'
source: 'https://academia.sh/en/courses/cloud-security/infrastructure-as-code-security'
course: 'Cloud and Virtualization Security'
language: en
updated: '2026-09-26T03:02:50+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates template fields into resolvable and unresolvable values, and shows which ones the rule set can decide on'
    - 'Separates the radius share caught before deployment from the share visible only while running'
    - "Measures the rule set's false-flagging cost and compares two ways of writing an exception"
---

# Infrastructure as Code Security

The wrong items found in the previous five lessons are put back into the template set and run through a rule set: 4 of the 11 wrong items are caught before deployment, closing 23 of a total 34-unit radius share (68%); 5 items resolve only while running; 2 items have no value in the template to read. The rule set falsely flags 3 legitimate items; disabling the rule erases both the false flag and the real finding.

Across five lessons, wrong items were found one at a time: an overly broad trust list, a public
endpoint left open, a permission forgotten at the object level, a key permission sitting on top of
the chain. All of them were found in the running system, and each one was already in place by the
time it was found. This lesson asks one last question: how many of them could have been seen **in
the template**, before they were ever put into the system.

Setting up infrastructure with templates was measured in the Infrastructure as Code course of the
DevOps and Platform Engineering curriculum; provisioning models and drift scanning are not repeated
here. The only distinction measured here is between **a value known before deployment** and **a
value known only while running**. A rule can only decide on the first.

- **CM28.** A template writes a resource's fields. A field's value is either **resolvable**
  (known by reading the template) or it is not: a deployment parameter, another template's output,
  a default set by the provider, a change made by hand outside the template, or a file's content.
- **CM29.** The **rule set** looks at specific fields. A rule's result is one of three classes:
  **caught** (resolvable value, the rule fired), **unknown** (the rule looks at the field but
  cannot resolve the value), **unseen** (no rule looks at that field).
- **CM30.** Each field's **radius share** is the radius reduction measured for that item in the
  previous five lessons. The numbers are read from there; they are not re-derived here.
- **CM31.** A field can also be legitimate. If the rule fires even though the item is correct,
  that is a **false flag**, and it stops a deployment.
- **CM32.** A while-running scan reads the value of fields in the `unknown` class from the
  running system; it cannot reach the `unseen` class. The scan is daily, and the average delay is
  taken as half a day.

## The Template Set Runs Through the Rule Set

```js
// cloud/template.mjs — the template set and rule set are a MODEL. Drift models and drift
// scanning are not in this lesson; what is measured is the item known before deployment versus
// the item known only while running.

// If a field's value starts with '<', it Cannot be resolved by reading the template: its value
// is set at deployment time or while running. radius = radius share measured in the previous
// five lessons. legitimate = whether the item is correct.
export const FIELD = `
  identity.tpl  role-archive         trust-list     role-backup     no      5
  identity.tpl  role-backup          trust-list     role-management no      3
  identity.tpl  account-collector    trust-list     role-archive    yes     0
  identity.tpl  role-management      key-permission k-secret        no     10
  identity.tpl  role-distribution    key-permission k-secret        yes     0
  store.tpl     template-store       public-access  open            no      5
  store.tpl     portal-asset-store   public-access  open            yes     0
  store.tpl     upload-preview       public-access  <inherited>     no      1
  store.tpl     measurement-store    public-access  <parameter>     no      1
  key.tpl       secret-store         public-endpoint <default>      no      1
  key.tpl       key-management       public-endpoint <default>      no      1
  network.tpl   private-endpoint-mapping target     <output>        no      2
  operations.tpl instance-management management-endpoint <out-of-template> no 1
  code.tpl      distribution-template file-content  <file>          no      4`
  .trim().split('\n').map((s) => {
    const [template, source, field, value, legitimate, radius] = s.trim().split(/\s+/);
    return { template, source, field, value, legitimate: legitimate === 'yes', radius: Number(radius) };
  });
export const resolvable = (a) => !a.value.startsWith('<');
// Rule set: which field it looks at, and when it fires for a resolvable value.
export const RULE = [
  ['trust-list', (d) => d.startsWith('role-')],
  ['key-permission', (d) => d === 'k-secret'],
  ['public-access', (d) => d === 'open'],
  ['public-endpoint', (d) => d === 'open'],
  ['target', () => false],
];
// Class: caught (resolvable value, rule fired), unknown (rule looks at the field but cannot
// resolve the value), unseen (no rule looks at that field).
export const classify = (a, exception = () => false) => {
  const k = RULE.find(([name]) => name === a.field);
  if (exception(a)) return 'unseen';
  if (k === undefined) return 'unseen';
  if (resolvable(a)) return k[1](a.value) ? 'caught' : 'clean';
  return 'unknown';
};
export const CLASS = ['caught', 'unknown', 'unseen'];
export const count = (c, exception, f = () => true) =>
  FIELD.filter((a) => f(a) && classify(a, exception) === c);
export const share = (l) => l.reduce((x, a) => x + a.radius, 0);
export const WRONG = FIELD.filter((a) => !a.legitimate);
```

```js
// cloud/run1.mjs — the template set run through the rule set: which item is known before deployment
import { FIELD, CLASS, WRONG, share, count, classify } from './template.mjs';

const print = (g, ...s) => console.log(s.map((v, i) =>
  g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i])).join(''));
const A = [-26, -21, -18, 8, 9, 12];
print(A, 'source', 'field', 'value', 'legit', 'radius', 'class');
for (const a of FIELD)
  print(A, a.source, a.field, a.value, a.legitimate ? 'yes' : 'no', a.radius, classify(a));
console.log(`\n${WRONG.length} of ${FIELD.length} fields are wrong; total radius share ${share(WRONG)}`);
const B = [-13, 9, 11, 10];
print(B, 'class', 'wrong', 'radius', 'share of');
print(B, '', 'item', 'share', 'percent');
for (const c of CLASS) {
  const l = count(c, undefined, (a) => !a.legitimate);
  print(B, c, l.length, share(l), `${Math.round(share(l) / share(WRONG) * 100)}%`);
}
const falsePositive = count('caught', undefined, (a) => a.legitimate);
console.log(`\nfalsely flagged legitimate item ${falsePositive.length}: ` +
  falsePositive.map((a) => a.source).join(', '));
```

```
source                    field                value                legit   radius       class
role-archive              trust-list           role-backup             no        5      caught
role-backup               trust-list           role-management         no        3      caught
account-collector         trust-list           role-archive           yes        0      caught
role-management           key-permission       k-secret                no       10      caught
role-distribution         key-permission       k-secret               yes        0      caught
template-store            public-access        open                    no        5      caught
portal-asset-store        public-access        open                   yes        0      caught
upload-preview            public-access        <inherited>             no        1     unknown
measurement-store         public-access        <parameter>             no        1     unknown
secret-store              public-endpoint      <default>               no        1     unknown
key-management            public-endpoint      <default>               no        1     unknown
private-endpoint-mapping  target               <output>                no        2     unknown
instance-management       management-endpoint  <out-of-template>       no        1      unseen
distribution-template     file-content         <file>                  no        4      unseen

11 of 14 fields are wrong; total radius share 34
class            wrong     radius  share of
                  item      share   percent
caught               4         23       68%
unknown              5          6       18%
unseen               2          5       15%

falsely flagged legitimate item 3: account-collector, role-distribution, portal-asset-store
```

Item count and radius share read in opposite directions in this table. Only four of the eleven
wrong items are caught before deployment — thirty-six percent of them. But those four items carry
twenty-three of the thirty-four-unit radius share, that is, sixty-eight percent of it. The reason
sits in a single row: the `k-secret` permission written to the `role-management` role carries ten
units and sits in the template as a plain value.

There is also a result in the opposite direction. The five items in the `unknown` class carry a
total of six units — one point two per item. They are numerous but contribute little to the
radius. If a rule set is measured by "how many findings did it close," the weight of these five
items is overstated; measured by "how many units of radius did it close," it falls into place.

The bottom row shows the rule set's other face. The same rules flag three legitimate items too:
the collector assuming the archive role, the distribution role using the secret key, and the
portal asset store being publicly open. All three are correct configuration, and all three stop a
deployment. The rule text cannot tell these three apart from the others, because the values are
identical.

## Two Ways to Write an Exception

```js
// cloud/run2.mjs — four rule-set variants and the share while-running scanning closes
import { WRONG, share, count } from './template.mjs';

const EXCEPTION_SOURCE = ['account-collector', 'role-distribution', 'portal-asset-store'];
const VARIANT = [
  ['no exception', () => false],
  ['source-level exception (3 lines)', (a) => EXCEPTION_SOURCE.includes(a.source)],
  ['key-permission rule disabled', (a) => a.field === 'key-permission'],
  ['all three rules disabled', (a) =>
    ['trust-list', 'key-permission', 'public-access'].includes(a.field)],
];
const print = (g, ...s) => console.log(s.map((v, i) =>
  g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i])).join(''));
const A = [-36, 9, 9, 10, 13, 13];
print(A, 'rule set', 'before', 'before', 'false', 'closed by', 'never');
print(A, '', 'item', 'share', 'flag', 'scan share', 'seen share');
for (const [name, exc] of VARIANT) {
  const y = count('caught', exc, (a) => !a.legitimate);
  const b = count('unknown', exc, (a) => !a.legitimate);
  print(A, name, y.length, share(y), count('caught', exc, (a) => a.legitimate).length,
    share(y) + share(b), share(WRONG) - share(y) - share(b));
}
console.log(`\ntotal wrong item ${WRONG.length}, total radius share ${share(WRONG)}`);
const unseen = count('unseen', undefined, (a) => !a.legitimate);
console.log(`fields no rule looks at: ` +
  unseen.map((a) => `${a.field} (${a.value.slice(1, -1)})`).join(', '));
console.log(`these ${unseen.length} items carry radius share ${share(unseen)}/${share(WRONG)} ` +
  `(${Math.round(share(unseen) / share(WRONG) * 100)}%); no value in the template to read`);
// Delay: 0 days for items caught before deployment, 0.5 days average for daily scanning, none for unseen.
const c0 = share(count('caught', undefined, (a) => !a.legitimate));
const u0 = share(count('unknown', undefined, (a) => !a.legitimate));
console.log(`weighted delay: ${c0} share 0 days, ${u0} share 0.5 days -> ` +
  `${(u0 * 0.5 / (c0 + u0)).toFixed(2)} days; remaining ${share(WRONG) - c0 - u0} share never seen`);
```

```
rule set                               before   before     false    closed by        never
                                         item    share      flag   scan share   seen share
no exception                                4       23         3           29            5
source-level exception (3 lines)            4       23         0           29            5
key-permission rule disabled                3       13         2           19           15
all three rules disabled                    0        0         0            4           30

total wrong item 11, total radius share 34
fields no rule looks at: management-endpoint (out-of-template), file-content (file)
these 2 items carry radius share 5/34 (15%); no value in the template to read
weighted delay: 23 share 0 days, 6 share 0.5 days -> 0.10 days; remaining 5 share never seen
```

The second row is the answer we are looking for. When the exception is written at the source
level — three names, three lines — false flagging drops from three to zero and the caught share
stays at twenty-three. The cost is maintaining a three-line list: every new legitimate item needs
one more line, and a name that is not dropped from the list will not be flagged even if it is no longer
legitimate.

The third and fourth rows show what the cheap way actually costs. Removing the false flag by
disabling the rule also erases the real finding that rule caught: when the `key-permission` rule
is disabled, the caught share drops from twenty-three to thirteen and the never-seen share rises
from five to fifteen. When all three rules are disabled, nothing is caught before deployment at
all. **Disabling the rule erases both the false flag and the real finding; writing an exception
erases only the false flag and leaves a maintenance burden.**

The last three lines draw the lesson's permanent boundary. Two items cannot be seen by any rule
set: a resource's management endpoint was opened by hand outside the template, and an identity
reference sits in a file's content. The rule set can be extended, but there is no value in the
template to read. These two items carry fifteen percent of the total radius share.

## Resolvability Varies by Template

The total is spread across six templates, and the spread is uneven.

```js
// cloud/run3.mjs — radius share and resolvability per template: where scanning pays off
import { FIELD, resolvable, share, classify } from './template.mjs';

const print = (g, ...s) => console.log(s.map((v, i) =>
  g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i])).join(''));
const A = [-14, 7, 11, 10, 11, 10];
print(A, 'template', 'field', 'resolvable', 'radius', 'caught', 'caught');
print(A, '', 'count', 'fields', 'share', 'share', 'ratio');
for (const s of [...new Set(FIELD.map((a) => a.template))]) {
  const l = FIELD.filter((a) => a.template === s), y = l.filter((a) => !a.legitimate);
  const caught = l.filter((a) => !a.legitimate && classify(a) === 'caught');
  print(A, s, l.length, `${l.filter(resolvable).length}/${l.length}`, share(y), share(caught),
    share(y) === 0 ? '-' : `${Math.round(share(caught) / share(y) * 100)}%`);
}
const c = FIELD.filter((a) => !a.legitimate && resolvable(a));
console.log(`\nresolvable wrong field ${c.length}/${FIELD.filter((a) => !a.legitimate).length}, ` +
  `share ${share(c)}/${share(FIELD.filter((a) => !a.legitimate))}; ` +
  `resolvability ranges from 0% to 100% per template`);
```

```
template        field resolvable    radius     caught    caught
                count     fields     share      share     ratio
identity.tpl        5        5/5        18         18      100%
store.tpl           4        2/4         7          5       71%
key.tpl             2        0/2         2          0        0%
network.tpl         1        0/1         2          0        0%
operations.tpl      1        0/1         1          0        0%
code.tpl            1        0/1         4          0        0%

resolvable wrong field 4/11, share 23/34; resolvability ranges from 0% to 100% per template
```

The identity template is extreme in two respects: it carries the largest radius share — eighteen
of the thirty-four — and all of its fields are resolvable. Scanning does its job there at a
hundred percent. In the store template, two of four fields are resolvable, and the caught ratio
drops to seventy-one percent.

In the bottom four rows the ratio is zero, and the reason is not the same. In the key template,
the fields were never written and are set by the default provider; in the network template, the
value comes from another template's output; the operations template's source was changed by hand
afterward; the code template points to a file, and the file's content is not a field. All four are
"not written in the template," but three of them can be resolved by scanning, and one cannot.

The practical consequence of this spread is where the scanning investment should go. More than
half the radius share sits in a single template, entirely in resolvable fields; a rule set that
looks at that template gives the highest return. Writing rules for the bottom four rows gains
nothing — the gain comes only from a mechanism that reads from the running system.

## Caught, Delayed, and Never Seen

The result splits into three parts. Twenty-three units of radius share close before deployment:
the deployment is blocked, the delay is zero days, and the wrong item never enters the system at
all. Six units resolve only while running; a daily scan sees them after half a day on average.
Five units are never seen at all.

The weighted delay is zero point one zero days, and that number can look misleading — the average
is small because most of the weight sits in the zero-delay class. But the average does not account
for the five units that are never seen at all; that share is not a delay, it is an absence.

The practical consequence of this split is this: template scanning can close two-thirds of the
radius share, and it does so with zero delay, but the remaining third needs another mechanism.
Part of that mechanism is while-running scanning; another part is a source the template never
knows about at all. Widening the rule set grows the first share; it does not change the second
share at all.

## Summary

- 11 of the 14 fields in the template are wrong, and the total radius share is 34 units. The 4
  items caught before deployment carry 23 (68%) of this share; item count and radius share read
  in opposite directions.
- The 5 items in the `unknown` class carry a total of 6 units — 1.2 per item. Measuring the rule
  set by "how many findings" overstates the weight of these items.
- The rule set also flags 3 legitimate items and stops 3 deployments; because the values are
  identical, the rule text cannot tell them apart.
- A source-level exception drops the false flag from 3 to 0 and leaves the caught share at 23;
  its cost is maintaining a 3-line list. Disabling the rule drops the caught share from 23 to 13.
- 2 items cannot be seen by any rule set — one is a management endpoint opened by hand outside the
  template, the other is an identity reference in a file's content. 15% of the total share sits
  here.
- The weighted delay is 0.10 days; the average is small because most of the weight sits in the
  zero-delay class. The 5 units that are never seen at all do not enter this average.

## Next Step

This topic carried one question all the way through: if one item is wrong, how many items can be
reached from it. The responsibility boundary was turned into a checklist, a walk was run from a
single leaked identity, the network path was intersected with permission, publicly open items
were counted, key ownership was measured in three modes, and finally, how many of the wrong items
could be seen before deployment was calculated. Each step produced a number, and the cost of each
check was written down.

All of these measurements have one thing in common: every one of them was done **by looking at
the configuration**. A trust list was read, a permission set was scanned, a template field was
resolved. Radius can even be measured when the configuration itself is correct — but then there is
nothing to measure, because there is no wrong item. The question that was not asked is this: if the
configuration is correct and a credential leaks anyway, how would this be known. At that moment,
no field is wrong, no rule fires, no scan finds anything; the only thing that can show whether the
walk actually ran is a **record**. The next topic, `Cloud Detection and Hardening`, begins with
that record: how many steps of the radius walk the management plane's audit log sees, and how many
it never sees at all.
