Skip to content
academia.sh

Lesson 09 / 15

Workload Identity

Workload identity is measured with a single number: how many static secrets it replaces. Twenty of the thirty-one items disappear, eleven remain, and all of the remainder gathers in six secrets — the keys that sit on the signature-verifying side and the external providers that do not support federation. Of the nine apps only two come to carry no static secret at all, and seven keep carrying at least one. The trust relation that replaces the secret does not reduce the item count; it reduces how leakable the item is — and when written at the group level it opens 36 pairs instead of 20.

Published Updated

In this lesson

  • Counts separately the static secret items that workload identity removes and the external system items that remain
  • Measures how many apps come to carry no static secret and how many keep carrying one
  • Evaluates the trust relation that replaces the secret along two axes: the breadth of the binding rule and the second single point that the identity provider opens
Contents

What was measured over two lessons was two properties of the same object: the life of the secret and the cost of changing it. Both assumed that the secret exists. This lesson removes that assumption.

The value an app carries to connect to a database is, in fact, a way of proving who that app is. If the proof comes not from a value the app itself carries but from the platform that runs the app, there is no value left to carry. The mechanics of workload identity — how the identity is given, how it is verified, which components are involved — were measured in an earlier course and are not repeated here. The measure here is a single number: how many static secrets it replaces, and how many it cannot.

  • SM18. The model of the previous three lessons continues: nine apps, thirty-one items, seventeen secrets. The environment is fictional, what is measured is a model written in node, and it carries no real secret value.
  • SM19. Whether a target system can verify workload identity directly is given in the model table and written with its reason. There are two reasons: the proof has to stay on the verifying side, and the provider does not support federation.
  • SM20. Signing keys are a special case. The party that verifies a document or session signature needs the key itself; workload identity authorizes a call, it does not have a signature verified. By contrast, the signature of a service-to-service call is authorization and is covered by workload identity.
  • SM21. Token life is taken as fifteen minutes; this is a fictional value, and a workload identity token is assumed not to be revocable before it expires.
  • SM22. There is no randomness; every number is derived from the model tables and the output is the same from run to run.

How Many Items Disappear

// identity.mjs — the static secrets that workload identity removes and the items that remain. A MODEL.
// The environment is FICTIONAL; THERE ARE NO REAL SECRET VALUES, only secret paths and acceptance properties are modeled.
export const READS = {
  "portal-frontend": ["signing/session-signing"],
  "portal-api": ["data/portal-primary", "data/portal-read", "signing/session-signing",
    "signing/internal-call-signing", "infra/cache", "infra/log-write"],
  "subscriber-signup": ["data/portal-primary", "external/email-gateway", "external/sms-gateway"],
  "billing-service": ["data/billing-primary", "data/portal-read", "data/archive",
    "external/payment-gateway", "signing/document-signing"],
  "payment-bridge": ["external/payment-gateway"],
  "field-api": ["data/field-primary", "queue/work-order-queue", "external/map-service",
    "signing/internal-call-signing"],
  "work-order": ["data/field-primary", "queue/work-order-queue", "queue/notification-queue",
    "external/map-service"],
  "notification": ["queue/notification-queue", "external/email-gateway", "external/sms-gateway"],
  "report-generator": ["data/portal-read", "data/archive", "infra/object-store",
    "external/email-gateway"],
};
// accepts: whether the target system can verify workload identity directly (yes/no + reason).
// rotation: the current rotation period (days) of a secret that cannot take an identity.
const DOMAIN_DEFAULT = { data: [true, "-", 180], queue: [true, "-", 180], infra: [true, "-", 180],
  signing: [false, "verifier-side", 90], external: [false, "provider-unsupported", 365] };
const EXCEPTIONS = { "infra/cache": [false, "provider-unsupported", 180],
  "signing/internal-call-signing": [true, "-", 90], "external/map-service": [true, "-", 365],
  "external/payment-gateway": [false, "provider-unsupported", 90] };
export const SECRETS = Object.fromEntries([...new Set(Object.values(READS).flat())].map((s) => {
  const [accepts, reason, rotation] = EXCEPTIONS[s] || DOMAIN_DEFAULT[s.split("/")[0]];
  return [s, { accepts, reason, rotation }];
}));
export const NAMES = Object.keys(READS);
export const ITEMS = NAMES.flatMap((a) => READS[a].map((s) => [a, s]));
export const remaining = (a) => READS[a].filter((s) => !SECRETS[s].accepts);
export const gen = (B) => (h) => h.map((v, j) => j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0])).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const row = gen([10, 9, 10, 11, 19, 26]);
  console.log(`model: ${ITEMS.length} (app, secret) items, ${Object.keys(SECRETS).length} secrets, ` +
    `${NAMES.length} apps (fictional production environment)\n`);
  console.log(row(["domain", "secrets", "removed", "remaining", "remaining secrets", "why it remains"]));
  for (const d of ["data", "queue", "infra", "signing", "external"]) {
    const k = ITEMS.filter(([, s]) => s.startsWith(d + "/"));
    const rm = k.filter(([, s]) => !SECRETS[s].accepts);
    const g = [...new Set(rm.map(([, s]) => SECRETS[s].reason))];
    console.log(row([d, [...new Set(k.map(([, s]) => s))].length, k.length - rm.length, rm.length,
      [...new Set(rm.map(([, s]) => s))].length, g.length ? g.join(",") : "-"]));
  }
  const K = ITEMS.filter(([, s]) => !SECRETS[s].accepts);
  console.log(row(["total", Object.keys(SECRETS).length, ITEMS.length - K.length, K.length,
    [...new Set(K.map(([, s]) => s))].length, ""]));

  const row2 = gen([19, 10, 11, 36]);
  console.log(`\n${row2(["app", "items", "remaining", "remaining secrets"])}`);
  for (const a of NAMES)
    console.log(row2([a, READS[a].length, remaining(a).length,
      remaining(a).map((s) => s.split("/")[1]).join(",") || "none"]));
  const clean = NAMES.filter((a) => remaining(a).length === 0);
  console.log(`\napps carrying no static secret: ${clean.length} / ${NAMES.length} ` +
    `(${clean.join(", ")})`);
  console.log(`apps carrying at least one static secret: ${NAMES.length - clean.length} / ${NAMES.length}`);
  const G = K.reduce((t, [, s]) => t + SECRETS[s].rotation, 0);
  const T = ITEMS.reduce((t, [, s]) => t + SECRETS[s].rotation, 0);
  console.log(`total exposure window: ${Math.round(T / 2)} -> ${Math.round(G / 2)} item-days ` +
    `(${(100 - 100 * G / T).toFixed(0)}% drop); mean per item ` +
    `${(T / ITEMS.length / 2).toFixed(0)} -> ${(G / K.length / 2).toFixed(0)} days`);
}
model: 31 (app, secret) items, 17 secrets, 9 apps (fictional production environment)

domain      secrets   removed  remaining  remaining secrets            why it remains
data              5        10          0                  0                         -
queue             2         4          0                  0                         -
infra             3         2          1                  1      provider-unsupported
signing           3         2          3                  2             verifier-side
external          4         2          7                  3      provider-unsupported
total            17        20         11                  6                          

app                     items  remaining                   remaining secrets
portal-frontend             1          1                     session-signing
portal-api                  6          2               session-signing,cache
subscriber-signup           3          2           email-gateway,sms-gateway
billing-service             5          2    payment-gateway,document-signing
payment-bridge              1          1                     payment-gateway
field-api                   4          0                                none
work-order                  4          0                                none
notification                3          2           email-gateway,sms-gateway
report-generator            4          1                       email-gateway

apps carrying no static secret: 2 / 9 (field-api, work-order)
apps carrying at least one static secret: 7 / 9
total exposure window: 3123 -> 1228 item-days (61% drop); mean per item 101 -> 112 days

The first two rows of the upper table give the expected result: all of the database and queue items, fourteen in number, disappear. These are internal systems, and they can learn who the workload is from the platform and authorize it directly. From the standpoint of secrets management these items no longer exist — nothing is stored, nothing is rotated, nothing leaks.

The signing row draws the distinction. Of the three signing secrets, one, the service-to-service call signature, is removed; because that signature is a proof of authorization and workload identity does the same job. The other two remain: the party that verifies the session signature and the party that verifies the document signature need the key itself. This distinction is often skipped, and it is where the claim “workload identity removes all keys” breaks. Workload identity authorizes a call; it does not have a signature verified.

The external row is the largest share of what remains: of the four external gateways only the map service supports federation, three give a fixed key, and those three secrets are spread over seven items. Compared with the previous lesson’s list of “eight secrets that cannot be shortened,” the table is consistent — the same provider limits, in the same items.

The lower table is the decision itself. The removal of twenty items leaves only two of the nine apps carrying no static secret: the field API and the work order service. The other seven apps keep carrying at least one static secret, and most carry only one or two. This is the most important finding of the measurement. While the item count falls by sixty-five percent, the number of apps carrying a secret falls only from nine to seven. The work of secrets management is proportional not to the item count but to the number of apps that carry a secret: every app must still have a secret distribution path, an owner, a rotation calendar, and a leak procedure. Saving two apps does not remove any of those seven procedures.

The last line says the same thing in terms of the exposure window. The total exposure falls from three thousand one hundred twenty-three item-days to one thousand two hundred twenty-eight, down sixty-one percent. But the mean window per item rises from one hundred one days to one hundred twelve: the removed items were internal items with short rotation periods, and the remaining ones are external keys with three hundred sixty-five days. The quality of what remains gets worse.

What Takes the Place of the Secret

The twenty removed items do not go into nothing; in place of each, a trust relation is written: a binding rule that says which workload identity the target system will accept.

// binding.mjs — the trust relation that replaces the secret: how many rules, how many pairs, how many single points
import { NAMES, READS, SECRETS, ITEMS, remaining, gen } from "./identity.mjs";

const TOKEN = 15;                      // life of the workload identity token (min, fictional)
export const GROUPS = { portal: ["portal-frontend", "portal-api", "subscriber-signup"],
  billing: ["billing-service", "payment-bridge"], field: ["field-api", "work-order"],
  back: ["notification", "report-generator"] };
const withIdentity = (a) => READS[a].filter((s) => SECRETS[s].accepts);

const row = gen([10, 11, 9, 13, 13, 17]);
console.log(row(["group", "workloads", "targets", "group rules", "group pairs", "workload rules"]));
let GR = 0, GP = 0, WR = 0;
for (const [g, members] of Object.entries(GROUPS)) {
  const targets = [...new Set(members.flatMap(withIdentity))];
  const wl = members.reduce((t, a) => t + withIdentity(a).length, 0);
  GR += targets.length; GP += members.length * targets.length; WR += wl;
  console.log(row([g, members.length, targets.length, targets.length,
    members.length * targets.length, wl]));
}
console.log(row(["total", NAMES.length, "", GR, GP, WR]));
console.log(`\ngroup-level binding: ${GR} rules, ${GP} (workload, target) pairs open`);
console.log(`workload-level binding: ${WR} rules, ${WR} pairs open; ` +
  `unused pairs ${GP - WR}`);
console.log(`in both cases the number of rules carrying a leakable value: 0 ` +
  `(a rule is an authorization record, not a secret)`);

const row2 = gen([19, 10, 16, 26]);
console.log(`\n${row2(["app", "identity", "static secrets", "single point bound to"])}`);
for (const a of NAMES) {
  const k = withIdentity(a).length, st = remaining(a).length;
  console.log(row2([a, k, st, k && st ? "store+identity" : k ? "identity" : "store"]));
}
const D = NAMES.filter((a) => remaining(a).length > 0), K = NAMES.filter((a) => withIdentity(a).length > 0);
console.log(`\nbound to the secret store ${D.length}, bound to the identity provider ${K.length}, ` +
  `to both ${NAMES.filter((a) => remaining(a).length && withIdentity(a).length).length}, ` +
  `bound to neither ${NAMES.filter((a) => !remaining(a).length && !withIdentity(a).length).length}`);
const removed = ITEMS.filter(([, s]) => SECRETS[s].accepts);
console.log(`today the exposure of the ${removed.length} removed items is ` +
  `${removed.reduce((t, [, s]) => t + SECRETS[s].rotation / 2, 0)} item-days; the life of the token ` +
  `replacing them is ${TOKEN} min, that is ${(removed.length * TOKEN / 2 / 1440).toFixed(2)} item-days`);
console.log(`apps that stop ${TOKEN} min into an identity provider outage: ${K.length} / ` +
  `${NAMES.length}; apps not affected by the outage at all: 0 / ${NAMES.length}`);
group       workloads  targets  group rules  group pairs   workload rules
portal              3        4            4           12                5
billing             2        3            3            6                3
field               2        5            5           10                8
back                2        4            4            8                4
total               9                    16           36               20

group-level binding: 16 rules, 36 (workload, target) pairs open
workload-level binding: 20 rules, 20 pairs open; unused pairs 16
in both cases the number of rules carrying a leakable value: 0 (a rule is an authorization record, not a secret)

app                  identity  static secrets     single point bound to
portal-frontend             0               1                     store
portal-api                  4               2            store+identity
subscriber-signup           1               2            store+identity
billing-service             3               2            store+identity
payment-bridge              0               1                     store
field-api                   4               0                  identity
work-order                  4               0                  identity
notification                1               2            store+identity
report-generator            3               1            store+identity

bound to the secret store 7, bound to the identity provider 7, to both 5, bound to neither 0
today the exposure of the 20 removed items is 1895 item-days; the life of the token replacing them is 15 min, that is 0.10 item-days
apps that stop 15 min into an identity provider outage: 7 / 9; apps not affected by the outage at all: 0 / 9

The upper table repeats, as is, the finding of the first lesson of this topic; only the object has changed. When the binding rule is written for a workload group, sixteen rules are enough but thirty-six (workload, target) pairs are opened; when it is written per workload, twenty rules are needed and exactly twenty pairs are opened. The sixteen unused pairs are the same form as the forty-eight unused grants of the first lesson: a rule written broadly means few lines and much access. Moving to workload identity does not solve this problem; it moves the problem from the secret path prefix to the workload group.

The third line says where the difference is. Even if the number of binding rules stays at twenty, a rule carries no leakable value. The exposure of the twenty removed items was one thousand eight hundred ninety-five item-days; the life of the token that replaces them is fifteen minutes, that is, zero point one item-days. The gain is not in the item count but in the quality of the item: an authorization record is not something that can be stolen, because when copied it is of no use — it takes the workload that carries the identity itself.

The lower table gives the cost, and the cost is familiar. The first lesson measured that the central secret store writes a shared startup dependency for nine apps. Workload identity does not remove this dependency, it raises it to two: seven apps are still bound to the secret store, seven apps to the identity provider, and five to both. The number of apps bound to neither is zero. The field API and the work order service are completely free of the secret store, but when the identity provider is unreachable for fifteen minutes both stop. The name of the dependency changed, its count did not.

Where This Decision Belongs

The removal of twenty items is not something that happens at runtime. No runtime defense caught, stopped, or flagged a secret; the items vanished because the deployment no longer produces them. This is the cheapest class of item measured in this course: not catching an item but never giving birth to it.

The work done in exchange is also at the deploy stage. The twenty binding rules are written into the configuration of the target systems and tied to the workload definition; once written, there is nothing left to watch at runtime. The only measurement that falls to runtime is the same as the one written in the first lesson: comparing the set of pairs the binding rules open with the calls actually made. If thirty-six pairs are opened and twenty are used, sixteen pairs are unused, and this can be seen only from the call log.

The remaining eleven items cannot be closed at any stage. Three external providers do not support federation, two signing keys sit on the verifying side, and the cache server does not accept identity. All of these items fall back on the procedures of the previous two lessons — the rotation calendar, the overlap window — and on the leak flow that is the subject of the next lesson.

Summary

  • Workload identity removes 20 of the 31 items and 11 remain. The remainder gathers in six secrets: two signing keys, three external gateway keys, and the cache server.
  • The distinction has a reason. The service-to-service call signature is an authorization and is removed; the session and document signature requires the key itself on the verifying side and remains. Of the four external gateways only one supports federation.
  • While the item count falls by 65%, the number of apps carrying a secret falls only from 9 to 7. The work of secrets management is proportional not to the item count but to the number of apps carrying a secret; the procedure of seven apps stays in place.
  • The total exposure window falls from 3,123 item-days to 1,228 (61%), but the mean window per item rises from 101 days to 112: the removed items were internal items, and the remaining ones are external keys.
  • The binding rule that replaces the secret does not reduce the count, it changes the quality: an exposure of 1,895 item-days falls to 0.1 item-days. But when the rule is written at the group level it opens 36 pairs with 16 lines; at the workload level 20 pairs with 20 lines. The 16 unused pairs are the same form as the 48 unused grants of the first lesson.
  • The dependency does not go away, it rises to two: 7 apps are bound to the secret store, 7 apps to the identity provider, 5 to both; the number of apps bound to neither is zero.

Next Step

This lesson removed most of the secrets, the previous two lessons shortened the life of the rest and wrote the rotation procedure. All three left the same question unanswered: what happens when a secret leaks. The exposure window was the time for a secret to become invalid on its own; if a leak is noticed and revoked, the window closes earlier. But noticing takes time, finding the owner takes time, producing the new one takes time, and the propagation of the new value takes as long as the overlap measured in the previous lesson. The next lesson runs this flow over the six remaining secrets: how many minutes pass from the moment of the leak to revocation, how many calls pass through the secret in those minutes, what shortening which stage gains, and how many legitimate requests are cut by revoking without waiting for propagation.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close