---
title: 'TLS Termination'
source: 'https://academia.sh/en/courses/servers-and-hardening/tls-termination'
course: 'Servers, Networking and Hardening'
language: en
updated: '2026-09-13T05:08:31+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares three termination points by plaintext-leg count, certificate-location count, and renewal-point count'
    - 'Counts the jobs termination moving away from the edge takes from the edge proxy'
    - "Models the accepted protocol version and cipher suite as an item set and derives the default's open-item count"
    - 'Measures separately how many items the version floor and the suite list each close, and how many devices each drops'
---

# TLS Termination

Comparing three options for where termination happens: the plaintext-leg count, certificate-location count, renewal-touch-point count, and the work the edge loses when terminating at the edge, at the proxy, and at the app; modeling protocol version and cipher suite as an item set, the twenty-eight items the default leaves open, and the device and read counts a restriction drops.

Every measurement in the previous lesson took one thing as given: encryption ends somewhere. The
edge proxy decrypts, everything behind it carries plaintext, and the lost information was handed
back through headers. "It ends at the edge proxy" is not an assumption, it is a decision, and
there are three places it can be made.

This lesson measures that decision with three numbers: how many legs of the chain stay plaintext,
how many separate places the certificate sits in, how many points get touched during renewal. The
three do not move in the same direction. The option that drops the plaintext legs to zero triples
the other two numbers.

Which security headers go into the response and how many do not arrive by default were measured
before, in the **Security Headers and TLS** lesson; that measurement is not repeated here. What is
measured here is not the header itself, but **where the encryption ends**.

The fictional regional measurement network's request path passes through four components: the
client, the edge proxy, the internal proxy that selects the environment, and the measurement app.
There are three legs.

- **WS13.** The component copy counts are fictional: the edge proxy has two copies, the internal
  proxy three, the app six.
- **WS14.** The number of points touched during renewal is taken as equal to the number of places
  the certificate sits in; the renewal flow's own steps are measured in the next lesson.
- **WS15.** Version names are fictional: `s1` is oldest, `s4` is newest. Cipher suites are named
  `t01`–`t12`, and each suite can be used from its own minimum version onward.
- **WS16.** An item is an accepted **(version, suite)** pair. The default policy sets no floor and
  accepts every suite.
- **WS17.** A client population drops if the highest version it supports is below the policy's
  floor, or if no suite it can use remains in the accepted list.
- **WS18.** The model does not measure time; no real handshake is performed, and the item and
  device counts are derived from the lists.

```js
// termination/point.mjs — model: the three options for TLS termination. The chain is the
// fictional measurement network's request path; the copy counts are fictional. The model counts
// the plaintext legs, how many places the certificate sits in, and how many points get touched during renewal.
const CHAIN = ["client", "edge", "internal-proxy", "app"];
const COPIES = { edge: 2, "internal-proxy": 3, app: 6 };  // instance count per component
// What the edge can do: things that need plaintext, and the one job visible during the handshake.
const CAPABILITY = [
  ["path-based routing", "plain"], ["edge cache", "plain"],
  ["per-path rate limit", "plain"], ["body-size check", "plain"],
  ["name-based routing", "handshake"],
];

function measure(point) {
  const i = CHAIN.indexOf(point);
  const legs = CHAIN.slice(0, -1).map((_, b) => (b < i ? "encrypted" : "plaintext"));
  const seesPlain = CHAIN.slice(i);                         // everything after termination sees plaintext
  const edgePlain = seesPlain.includes("edge");
  return {
    plaintextLegs: legs.filter((x) => x === "plaintext").length,
    certLocations: COPIES[point],
    renewalPoints: COPIES[point],
    plainViewers: seesPlain.length,
    edgeCapability: CAPABILITY.filter(([, k]) => k === "handshake" || edgePlain).length,
  };
}

const E = [16, 16, 16, 18, 17, 18];
const write = (h) => h.map((d, i) => String(d)[i ? "padStart" : "padEnd"](E[i])).join("");
console.log(write(["termination", "plaintext legs", "cert locations", "renewal points",
  "sees plaintext", "edge capability"]));
for (const n of ["edge", "internal-proxy", "app"]) {
  const o = measure(n);
  console.log(write([n, o.plaintextLegs, o.certLocations, o.renewalPoints, o.plainViewers,
    `${o.edgeCapability}/${CAPABILITY.length}`]));
}

// --- Protocol and cipher suite: item set ---------------------------------
// Version names are fictional; s1 is oldest, s4 is newest. A suite can only be used from
// its own minimum version onward.
const SUITE = [
  ["t01", 1, false], ["t02", 1, false], ["t03", 2, false], ["t04", 2, false],
  ["t05", 2, false], ["t06", 3, true],  ["t07", 3, true],  ["t08", 3, false],
  ["t09", 3, true],  ["t10", 4, true],  ["t11", 4, true],  ["t12", 4, true],
];
// Fictional client population: [name, device count, highest version supported, daily reads]
const CLIENT = [
  ["old-field-handheld", 40, 1, 1200], ["billing-client", 25, 2, 400],
  ["new-field-handheld", 180, 3, 5400], ["work-order-app", 95, 3, 950],
  ["collector", 60, 4, 28800], ["browser-dashboard", 300, 4, 900],
];

function policy(minVersion, strongOnly) {
  const acceptedSuites = SUITE.filter(([, , g]) => (strongOnly ? g : true));
  const items = [];
  for (let s = minVersion; s <= 4; s += 1)
    for (const [name, min, strong] of acceptedSuites) if (min <= s) items.push([s, name, strong]);
  const connected = CLIENT.filter(([, , maxV]) =>
    maxV >= minVersion && acceptedSuites.some(([, min]) => min <= maxV));
  const dropped = CLIENT.filter((x) => connected.includes(x) === false);
  return {
    items: items.length, weak: items.filter((k) => k[2] === false).length,
    devices: connected.reduce((t, x) => t + x[1], 0),
    droppedDevices: dropped.reduce((t, x) => t + x[1], 0),
    droppedReads: dropped.reduce((t, x) => t + x[3], 0),
    droppedNames: dropped.map((x) => x[0]),
  };
}

const P = [13, 15, 16, 13, 18, 15, 20];
const write2 = (h) => h.map((d, i) => String(d)[i ? "padStart" : "padEnd"](P[i])).join("");
console.log("\n" + write2(["min version", "suite list", "accepted items", "weak items",
  "connected dev", "dropped dev", "dropped reads/day"]));
for (const [min, strong] of [[1, false], [2, false], [3, false], [3, true], [4, true]]) {
  const r = policy(min, strong);
  console.log(write2([`s${min}`, strong ? "strong only" : "all", r.items, r.weak, r.devices,
    r.droppedDevices, r.droppedReads]) + (r.droppedNames.length ? `   ${r.droppedNames.join(", ")}` : ""));
}
```

```
termination       plaintext legs  cert locations    renewal points   sees plaintext   edge capability
edge                           2               2                 2                3               5/5
internal-proxy                 1               3                 3                2               1/5
app                            0               6                 6                1               1/5

min version       suite list  accepted items   weak items     connected dev    dropped dev   dropped reads/day
s1                       all              28           19               700              0                   0
s2                       all              26           17               660             40                1200   old-field-handheld
s3                       all              21           12               635             65                1600   old-field-handheld, billing-client
s3               strong only               9            0               635             65                1600   old-field-handheld, billing-client
s4               strong only               6            0               360            340                7950   old-field-handheld, billing-client, new-field-handheld, work-order-app
```

## Three Options, Three Numbers

The first table lines up the three options side by side, and no row beats the others in every
column.

**Terminating at the edge** is the most common arrangement and the one that asks for the least
maintenance: the certificate sits in two places, renewal touches two points. Its cost is two
plaintext legs. On the path from the edge proxy to the app, meter readings, subscriber identities,
and invoice amounts travel as plaintext. This path is called the "internal network," and treating
the internal network as trustworthy is not a measurement, it is a habit. The number of components
that can see plaintext is three.

**Terminating at the proxy** reduces the edge proxy to a carrier: the encrypted connection passes
through it unchanged and ends at the internal proxy. The plaintext leg drops to one, the number of
components that see plaintext drops to two. In return, the certificate starts sitting in three
places and renewal touches three points.

**Terminating at the app** drops the plaintext leg to zero. Only the app sees plaintext. The
certificate sits in six places and renewal touches six points. The number has tripled, and what is
growing is not a security setting, it is **a maintenance surface**: a key distributed to six
places is six separate copies, six separate file permissions, six separate chances for renewal to
fail.

The three rows show a single trend: as the plaintext leg shrinks, the number of places the key
sits in grows. Zero plaintext legs does not mean zero risk; it moves the risk from the transport
path to **key distribution**.

The reason the two numbers do not move in the same direction is structural. Encrypting a leg
requires the component at that leg's receiving end to be able to decrypt it; being able to decrypt
requires holding the key. The deeper encryption reaches into the chain, the more copies the key
lands in. The component count also grows toward the end of the chain — the edge has two copies,
the internal proxy three, the app six — and this growth is not a coincidence: the layer that
scales is the app, not the edge.

## What the Edge Loses

The last column shows the decision's other cost. The edge proxy does five jobs, and four of them
require reading the request's content: path-based routing, the edge cache, the per-path rate
limit, the body-size check. The fifth rests on the server name visible during the handshake, and
it can be done without decrypting.

When termination happens at the edge, all five jobs can be done. When termination moves away from
the edge, **all four jobs drop at once**, and the second and third rows give the same value in
this column: `1/5`.

This is the course's "wrong closure" column, and the wrong closure here is large. The step that
closes the plaintext leg closes the edge cache — every bit of edge hit rate the next lesson
measures drops to zero. It closes the per-path rate limit — an abusive client can now only be
stopped at the app, and by the time it gets there it has crossed the entire chain. It closes the
body-size check — a large body now travels all the way to one of the six app copies.

The question to ask when deciding among the three options is not "which is more secure"; the
numbers do not give that question a single answer. The question is: accept two plaintext legs and
do four jobs at the edge, or close the leg and move those four jobs into the app. When the second
path is chosen, the app has to carry its own rate limit, its own body check, and its own cache,
and that is the new surface closing brings.

## The Surface Closing Brings

Closing the plaintext leg spawns new items, and they do not show up in the table.

While the certificate sits in two places, a renewal has either been applied to both copies or to
neither. While it sits in six places, a **partial state** is possible: four copies renewed, two
still standing on the old key. This in-between state does not produce a failure — as requests are
distributed, they are met with different certificates depending on which copy they land on, and a
client watching from outside only sees this when it lands on the copy whose certificate has
expired. As the renewal-point count grows, the failure's **probability of showing up** does not
grow, its **time to be found** grows longer.

The second new item is key distribution. A file that can be placed by hand into two copies needs a
distribution path to reach six copies, and that path is a new authority: some component has to be
able to read the key and write it to six places. The key sitting in six places is not one item, it
is seven items — six copies and the single path that writes them.

Third is the certificate becoming tied to the app's version. While termination happens at the app,
renewing the certificate requires reloading the app; two separate calendars — the release calendar
and the renewal calendar — touch the same process.

## Protocol as an Item Set

The second table measures another default. Once the termination point is decided, the terminating
component decides **which handshakes to accept**, and in most setups no one makes that decision.

The default row is the first row: no floor, every suite accepted. Twenty-eight items are open, and
**nineteen of them are weak**. All seven hundred devices can connect, no reads drop. This is
exactly the default's appeal: zero breakage. No one complains, and the nineteen weak items sit in
a list no one looks at, because no one complains.

Setting the floor to `s2` drops the item count to twenty-six: two items closed. The cost is forty
old handheld terminals and twelve hundred reads a day. The `s3` floor drops it to twenty-one
items, sixty-five devices drop, sixteen hundred reads a day are cut — and **twelve weak items are
still open**.

This is the measure of why the version floor is not enough by itself. The floor closes an item by
looking at its version; a weak suite keeps being accepted under a newer version too. Suite number
eight can be used starting from `s3`, and it is weak; the version floor never closes it.

## Twelve Items Closed for Free

The fourth row is the measurement's sharpest result. The floor is held fixed at `s3`, and only the
suite list is restricted to the strong ones.

The accepted item count drops from twenty-one to **nine**, the weak item count drops from twelve
to **zero**, and the dropped-device count does not change at all: sixty-five. The dropped-read
count stays the same too. Twelve items closed with no additional breakage.

This is the difference between the two levers. The version floor is expensive: every step drops a
client population. The suite list is cheap: removing weak suites drops no one as long as strong
suites remain that clients speaking the same version can use. The hardening order should follow
this — the suite list first, the version floor second.

Reversing the order does measurable damage. A team that raises the floor first drops sixty-five
devices, the phones ring, the step gets rolled back, and the weak suites stay open too — breakage
happened, no item closed. If the same team had narrowed the suite list first, it would have closed
twelve items with zero breakage, and would then be having the floor argument without those twelve
items in the way.

The fifth row shows where the limit sits. When the floor is raised to `s4`, the item count drops
to six, but three hundred forty devices drop and seventy-nine hundred fifty reads a day are cut.
The price paid to go from nine items to six is losing half the system. The fourth row closed
twelve items with zero additional devices lost; the fifth row closes three items with two hundred
seventy-five additional devices lost. This difference between two steps in the same table shows
why treating the item count alone as a target is misleading.

The item that cannot be closed appears here by name: **forty old handheld terminals**. These
devices cannot speak anything above `s1`, and until they are replaced, the floor cannot rise above
`s1`. The only way out is opening a separate listener for them — accepting the old version only on
that listener. This is not a fix, it is a relocation: the surface item does not close, it changes
address, and one more item gets added to the **listening port** list the previous lesson counted.

## Summary

- As termination moved from the edge toward the app, the plaintext-leg count dropped from two to
  zero, while the certificate-location count and the renewal-touch-point count climbed from two
  to six.
- The number of components able to see plaintext dropped from three to one; zero plaintext legs
  moved the risk from the transport path to key distribution.
- When termination moved away from the edge, four of the edge proxy's five jobs dropped:
  path-based routing, the edge cache, the per-path rate limit, and the body-size check.
- The default policy accepted twenty-eight items and nineteen were weak; because no device
  dropped, the default was never questioned.
- Setting the version floor to `s3` closed seven items, dropped sixty-five devices, and left
  twelve weak items open; restricting the suite list to the strong ones closed all twelve of the
  remaining weak items **with no additional device loss**.
- Forty old handheld terminals kept the floor from rising above `s1`; opening a separate listener
  for them does not close the item, it adds one to the listening-port list.

## Next Step

This lesson counted **how many places** the certificate sits in and took the renewal-touch-point
count as equal to that number. Renewal itself stayed a black box: it happens some day, it
finishes, the certificate is refreshed.

Inside that box is a state machine with two settings: the certificate's lifetime and how many days
before expiry renewal starts. If the threshold is set too early, unnecessary renewals run; if it
is set too late, renewal may not finish before expiry, raising the question of **how many hours of
exposure window** are left in between. There is a validation step in the middle of the renewal
flow, and that step can fail; how many attempts are made, on which attempt the certificate
expires, and when the alert should arrive are separate numbers. The next lesson writes the renewal
flow as a state machine in `node`, sweeps lifetime against threshold, models validation failure
and measures the exposure window in hours, and counts how many items manual renewal gets forgotten
on over a period.
