Skip to content
academia.sh

Lesson 10 / 12

Serverless Security

Measuring the window in which evidence can be collected for short-lived workloads: the median lifetime of 400 calls is 447 ms; manual collection tied to an alert catches none of the 400 calls, automatic collection catches 15. Continuous streaming from within the environment takes 3,541 of 3,600 evidence items; a 60-second hold gives the same result at 34 times the running time in environment-seconds. The identity's validity period, meanwhile, is 8,054 times the lifetime, and one hundred percent of the open item-seconds pass after the workload has ended.

Published Updated

In this lesson

  • Separates evidence items in short-lived workloads by collectibility class, and counts the calls the collection delay catches
  • Compares two ways of extending the collection window: measures the environment-second cost of holding the environment and the per-call delay of streaming from within it
  • Compares the identity's validity period with the workload's lifetime to compute open item-seconds, and states the factor by which cancellation lowers it
Contents

The previous lesson’s entire measurement assumed that the workload is standing: it sits on the host, its identity is usable, its neighbor is countable. One class of workload does not satisfy this assumption. In a function-execution environment, a workload lives from a hundred milliseconds to a few seconds, finishes its work, and the environment is reclaimed. There is no process to look at, no shell to open, no disk to image once the alert arrives.

What this lesson measures is therefore the window: how many seconds the workload lives, and how many of the evidence items never come back if they are not collected before that window closes. The runtime constraints, cold start, and scaling behavior of short-lived workloads were measured in the Serverless and Edge Computing course; they are not repeated here. The measurement is on the regional measurement network, is fictional, and is a model written in node; the generator is self-written, the seed appears in the output.

CD19. The model consists of 400 calls; lifetimes are generated from three bands and are in milliseconds (fictional). CD20. Nine evidence items fall into three classes: five can only be collected while the workload is live, three can also be collected afterward, and one is the workload’s own log, delivered only if it is flushed before termination. CD21. Collection delay TT is the time from the alert to the start of collection; hold HH is how long the environment is kept alive after the call ends. CD22. Items collectible while live are only collected if T<lifetime+HT < \text{lifetime} + H. CD23. Every call gets its own short-lived identity; the token’s validity period is VV seconds (fictional). CD24. Open item-seconds is the product of the number of items the identity reaches and the duration the token stays valid. The number of items reached is taken from the walk in the first lesson.

The Window in Which Evidence Can Be Collected

// lifetime.mjs — the window in which evidence can be collected for short-lived workloads. IT IS A MODEL.
// The regional measurement network is FICTIONAL. The generator is self-written; the seed appears below.
export const SEED = 20260805;
const generator = (t) => { let s = t >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };

// Evidence item: name|class. live: collectible while the workload is standing; afterward: also
// collectible later; flush: the workload's own log, delivered only if flushed before termination.
export const EVIDENCE = `
  memory-contents|live          process-list|live
  open-connection|live           environment-variable-value|live
  temp-file|live            call-metadata|afterward
  management-plane-log|afterward   function-version|afterward
  function-own-log|flush`.trim().split(/\s{2,}|\n/).map((x) => x.trim().split("|"))
  .map(([name, cls]) => ({ name, class: cls }));

const r = generator(SEED);
// 400 calls: lifetime in milliseconds, three bands. The flush flag says whether the log is
// delivered or not (fictional ratio).
export const CALL = Array.from({ length: 400 }, () => {
  const u = r();
  const lifetime = u < 0.72 ? 80 + Math.floor(r() * 520)
    : u < 0.94 ? 600 + Math.floor(r() * 3400)
    : 4000 + Math.floor(r() * 41000);
  return { lifetime, flushed: r() > 0.14 };
});

export const LIVE = EVIDENCE.filter((k) => k.class === "live").length;
export const AFTERWARD = EVIDENCE.filter((k) => k.class === "afterward").length;

// How many evidence items are collected in a given setting. T: collection delay (ms), H: hold
// after the call ends (ms).
export function collect(T, H) {
  let alive = 0, items = 0;
  for (const c of CALL) {
    const window = c.lifetime + H;
    if (T < window) { alive++; items += LIVE; }
    items += AFTERWARD + (c.flushed ? 1 : 0);
  }
  return { alive, items, total: CALL.length * EVIDENCE.length,
    envSeconds: Math.round((CALL.length * H) / 1000) };
}

const gen = (W) => (row) => row.map((v, j) => j ? String(v).padStart(W[j]) : String(v).padEnd(W[0]))
  .join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const sorted = [...CALL].map((c) => c.lifetime).sort((a, b) => a - b);
  const pct = (p) => sorted[Math.floor(p * sorted.length)];
  console.log(`model (seed ${SEED}): ${CALL.length} calls, ${EVIDENCE.length} evidence items ` +
    `(${LIVE} live, ${AFTERWARD} afterward, 1 flush-dependent)`);
  console.log(`lifetime (ms): median ${pct(0.5)}, 90% ${pct(0.9)}, 99% ${pct(0.99)}, ` +
    `longest ${sorted.at(-1)}; total lifespan ${Math.round(sorted.reduce((a, b) => a + b) / 1000)} s`);

  const tbl = gen([40, 10, 10, 22, 18, 16]);
  console.log("\n" + tbl(["setting", "delay", "hold", "evidence collectible", "items collected",
    "cost"]));
  const SETTING = [
    ["alert + manual collection", 300000, 0, "-"],
    ["automatic collection", 10000, 0, "-"],
    ["automatic collection + hold", 10000, 60000, "env-sec"],
    ["automatic collection + long hold", 2000, 300000, "env-sec"],
    ["continuous streaming from environment", 0, 0, "8 ms per call"],
  ];
  for (const [name, T, H, cost] of SETTING) {
    const s = collect(T, H);
    console.log(tbl([name, `${T / 1000} s`, `${H / 1000} s`,
      `${s.alive}/${CALL.length} calls`, `${s.items}/${s.total}`,
      H ? `${s.envSeconds} env-sec` : cost]));
  }

  const manual = collect(300000, 0), auto = collect(10000, 0), stream = collect(0, 0);
  console.log(`\nmanual collection catches ${manual.alive}/${CALL.length} calls, automatic ` +
    `collection ${auto.alive}/${CALL.length}; the difference between the two is ` +
    `${auto.items - manual.items} items`);
  console.log(`streaming from within the environment catches ${stream.alive}/${CALL.length} ` +
    `calls and collects ${stream.items}/${stream.total} items: the ${stream.total - stream.items} ` +
    `items that remain missing are the own-log not flushed before termination`);
  const hold = collect(10000, 60000);
  console.log(`a 60 s hold takes the calls with collectible evidence from ${auto.alive} -> ` +
    `${hold.alive}; its cost is ${hold.envSeconds} environment-seconds, that is ` +
    `${Math.round(hold.envSeconds / (sorted.reduce((a, b) => a + b) / 1000))} times the running time`);
}
model (seed 20260805): 400 calls, 9 evidence items (5 live, 3 afterward, 1 flush-dependent)
lifetime (ms): median 447, 90% 3249, 99% 29130, longest 40604; total lifespan 703 s

setting                                      delay      hold  evidence collectible   items collected            cost
alert + manual collection                    300 s       0 s           0/400 calls         1541/3600               -
automatic collection                          10 s       0 s          15/400 calls         1616/3600               -
automatic collection + hold                   10 s      60 s         400/400 calls         3541/3600   24000 env-sec
automatic collection + long hold               2 s     300 s         400/400 calls         3541/3600  120000 env-sec
continuous streaming from environment          0 s       0 s         400/400 calls         3541/3600   8 ms per call

manual collection catches 0/400 calls, automatic collection 15/400; the difference between the two is 75 items
streaming from within the environment catches 400/400 calls and collects 3541/3600 items: the 59 items that remain missing are the own-log not flushed before termination
a 60 s hold takes the calls with collectible evidence from 15 -> 400; its cost is 24000 environment-seconds, that is 34 times the running time

The median call lives 447 milliseconds; nine-tenths of calls finish under 3.2 seconds. In this distribution, manual collection tied to an alert — with a five-minute delay — does not catch any of the 400 calls. Zero. When automatic collection brings the delay down to ten seconds, it catches 15 calls, that is, 3.75% of them; the 75 evidence items this gains are 2.1% of the total 3,600 items.

The Item Collection Never Catches

How stark this number is shows that this is not a problem that shortening the delay fixes. A ten-second delay is already a fast response, and it still misses 96% of calls; dropping to two seconds is not enough either, because the median lifetime is half a second. Shortening the collection delay is a lever that does not work for this workload class.

The blind prefix from the first lesson takes another form here. There, the log’s delay made the first steps of the walk invisible, but the steps still sat in the log afterward; here, the delay causes the evidence to never exist at all. The alert arriving 5 minutes later does not delay the log — it eliminates the thing to be collected. The 1,541 items collected come only from the three classes that can also be collected afterward, and from flushed own-logs; all 2,000 of the items collectible only while live are lost.

There are two levers that work, and neither of them extends the window — they remove it. The first is hold: if the environment is kept alive for sixty seconds after the call ends, the number of calls with collectible evidence goes from 15 to 400, and the items collected go from 1,616 to 3,541. The cost is 24,000 environment-seconds — while the 400 calls’ total running time is 703 seconds, the hold consumes 34 times that many environment-seconds. What is more, raising the hold to 300 seconds brings no additional items and raises the cost to 120,000 environment-seconds: five times the price for the same result.

The second is continuous streaming from within the environment: the items collectible while live are written out as the workload runs. This gives the same 3,541 items with an added delay of 8 milliseconds per call. The 59 items that remain missing are the same across all three settings and come from a single class — the own-log not flushed before termination. Neither hold nor streaming saves it; flushing the log before termination is something the workload itself has to do.

The Radius Window Is Longer Than the Evidence Window

// token.mjs — the workload's lifetime is compared with the validity period of the identity it gets. IT IS A MODEL.
import { CALL } from "./lifetime.mjs";

// The number of items the identity reaches is taken from the walk run in the first lesson: MODEL input.
const ACCESS = { collection: 5, narrowed: 2 };
const TOTAL_LIFETIME = CALL.reduce((t, c) => t + c.lifetime, 0) / 1000;   // seconds

// Open item-seconds in a given setting: item count x the duration the token stays valid, per call.
// The cancel option invalidates the token as soon as the workload ends.
function measure({ V, cancel = false, narrow = false }) {
  const items = ACCESS[narrow ? "narrowed" : "collection"];
  let open = 0, deadOpen = 0;
  for (const c of CALL) {
    const duration = cancel ? c.lifetime / 1000 : V;
    open += items * duration;
    deadOpen += items * Math.max(0, duration - c.lifetime / 1000);
  }
  return { open: Math.round(open), deadOpen: Math.round(deadOpen), items,
    share: `${Math.round(100 * deadOpen / open)}%` };
}

const gen = (W) => (row) => row.map((v, j) => j ? String(v).padStart(W[j]) : String(v).padEnd(W[0]))
  .join("");
const tbl = gen([32, 12, 8, 20, 12, 30]);

console.log(`${CALL.length} calls, total running time ${TOTAL_LIFETIME.toFixed(0)} s; every call ` +
  `gets its own short-lived identity`);
console.log("\n" + tbl(["setting", "validity", "items", "open item-seconds", "dead share", "cost"]));
const SETTING = [
  ["default token", { V: 3600 }, "none"],
  ["validity 900 s", { V: 900 }, "renewal 4x"],
  ["validity 300 s", { V: 300 }, "renewal 12x"],
  ["cancel when workload ends", { V: 3600, cancel: true }, `${CALL.length} cancellation calls`],
  ["cancel + narrowed identity", { V: 3600, cancel: true, narrow: true }, "1 legitimate task rewritten"],
];
for (const [name, a, cost] of SETTING) {
  const s = measure(a);
  console.log(tbl([name, a.cancel ? "= lifetime" : `${a.V} s`, s.items, s.open, s.share, cost]));
}

const baseline = measure({ V: 3600 }), canceled = measure({ V: 3600, cancel: true });
const narrowed = measure({ V: 3600, cancel: true, narrow: true });
const median = [...CALL].map((c) => c.lifetime).sort((a, b) => a - b)[CALL.length >> 1];
console.log(`\nthe evidence window and the radius window are not on the same scale: the median ` +
  `call lives ${median} ms, the token it gets is valid for 3600 s — ` +
  `${Math.round(3600000 / median)} times`);
console.log(`in the default setting, ${baseline.share} of the open item-seconds is time that ` +
  `passes AFTER the workload ends; making the validity 300 s brings this share to ` +
  `${measure({ V: 300 }).share}`);
console.log(`canceling when the workload ends brings the open item-seconds from ${baseline.open} ` +
  `-> ${canceled.open}, that is ${Math.round(baseline.open / canceled.open)} times; its cost is ` +
  `one cancellation call per call`);
console.log(`narrowing the identity (5 -> 2 items) brings the same window from ${canceled.open} ` +
  `-> ${narrowed.open}; the two controls work multiplicatively, together ` +
  `${Math.round(baseline.open / narrowed.open)} times`);
400 calls, total running time 703 s; every call gets its own short-lived identity

setting                             validity   items   open item-seconds  dead share                          cost
default token                         3600 s       5             7200000        100%                          none
validity 900 s                         900 s       5             1800000        100%                    renewal 4x
validity 300 s                         300 s       5              600000         99%                   renewal 12x
cancel when workload ends         = lifetime       5                3514          0%        400 cancellation calls
cancel + narrowed identity        = lifetime       2                1405          0%   1 legitimate task rewritten

the evidence window and the radius window are not on the same scale: the median call lives 447 ms, the token it gets is valid for 3600 s — 8054 times
in the default setting, 100% of the open item-seconds is time that passes AFTER the workload ends; making the validity 300 s brings this share to 99%
canceling when the workload ends brings the open item-seconds from 7200000 -> 3514, that is 2049 times; its cost is one cancellation call per call
narrowing the identity (5 -> 2 items) brings the same window from 3514 -> 1405; the two controls work multiplicatively, together 5125 times

The table gives the lesson’s real number. The workload lives 447 milliseconds; the token it gets is valid for 3,600 seconds: 8,054 times as long. The evidence window is measured in milliseconds while the radius window is measured in hours, and the two are faces of the same event. In the default setting, nearly all of the open item-seconds — rounded, one hundred percent of them — is time that passes after the workload has ended. A valid credential sits there while nothing is running.

Separating the Two Windows

Shortening the validity period is a lever in the right direction, but a slow one: dropping from 3,600 seconds to 300 seconds lowers the open item-seconds twelvefold, while the dead share only drops from 100% to 99%, and in return the number of token renewals rises twelvefold. The ratio does not change because the problem is not the length of the period, but that the period is not tied to the workload’s lifetime.

Tying it works. Canceling the token as soon as the workload ends brings the open item-seconds down from 7,200,000 to 3,514 — 2,049 times — and zeroes out the dead share. The cost is one cancellation call per call, that is, 400 extra management-plane calls for 400 calls. The last row adds the second lever: lowering the number of items the identity reaches from 5 to 2 pulls the window down to 1,405. The two controls work multiplicatively; together, 5,125 times. The cost is a legitimate task being rewritten.

The lesson’s two tables thus arrive at the same sentence: in a short-lived workload, the evidence window closes on its own, the radius window does not. Keeping the first one open is something you pay for; closing the second one is something you decide to do.

Summary

  • The median lifetime of 400 calls is 447 ms, 90% of them under 3.2 seconds; five of the nine evidence items can only be collected while the workload is live.
  • With a five-minute delay, manual collection does not catch any of the 400 calls; a ten-second automatic collection catches 15 of them and gains only 2.1% of the total 3,600 items.
  • Holding the environment for 60 seconds raises the calls with collectible evidence to 400 and collects 3,541 items; the cost is 24,000 environment-seconds, 34 times the total running time, and raising it to 300 seconds brings no additional items.
  • Continuous streaming from within the environment gives the same 3,541 items with an added 8 ms delay per call; in all three settings, the 59 items that remain missing are the own-log not flushed before termination.
  • The token’s validity period is 8,054 times the median lifetime, and in the default setting, nearly all of the open item-seconds pass after the workload has ended.
  • Shortening the period does not change the ratio (at 300 seconds the dead share is still 99%); canceling when the workload ends lowers the window 2,049-fold, and narrowing the identity lowers it 5,125-fold together.

Next Step

Up to this point, every lesson picked a single misconfigured item and measured its radius: an overprivileged role, an open bucket, an environment that is not held, a long-lived token. These were measured one by one, but how they connect to each other was not written down. The next lesson gathers these connections at the pattern level: which item being wrong opens which path, which log falls along that path, and which control closes the path.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close