In short

Send every update, including rules, signatures and configuration that privileged code interprets, through rings: an internal ring, a small canary that mirrors the fleet, then progressively larger rings, each with a bake time and automatic halt on crash, boot-failure or heartbeat regressions against a control group. Validate content in CI against the interface of the code that will run it, and keep a kill switch that stops distribution. Make rollback automatic and make it work on a host that can no longer boot or reach the network, while refusing downgrades below a security floor. Let customers choose their ring. The SSDF 1.2 draft adds this as practice PS.4.

  • Urgent does not mean global: an emergency path compresses bake times; it does not skip the canary.
  • If a file changes what privileged code does, it is code for release purposes, whatever its extension.
  • Check content against the consuming code's real interface, generated from that code, and exercise every field with a non-wildcard value.
  • Judge a ring against a control group on crash, boot and heartbeat signals, and halt automatically; humans decide to resume, not to stop.
  • Rollback must work on a host the update broke: keep last-known-good locally, use boot counters or A/B slots, and keep an anti-rollback floor.
  • SSDF 1.1 has no staged-delivery practice; the SSDF 1.2 initial public draft adds PS.4 (tiered rollout, rollback, robust update engines), still a draft as of October 2026.

Verify it

  1. Pick your last three updates of each kind (code, rules or signatures, configuration) and confirm from the deployment logs that each went through every ring with its bake time.
  2. Inject a crash into a test build and confirm the pipeline halts at the canary ring without a human acting, and that hosts in that ring return to the previous version.
  3. Run CI with a content file that references a field the consuming code does not supply, and confirm the build fails.
  4. Power-cycle a test host on a deliberately broken update until it falls back to last-known-good, with the network disconnected.
  5. Try to install a release below your anti-rollback floor and confirm the client refuses it.
Applies to

Teams that ship updates to fleets they do not fully control: endpoint and security agents, client apps and SDKs, detection rules and signatures, WAF and edge rules, feature flags and remote configuration, and device firmware.

Learning goals and prerequisitesAbout 35 minutes: 15 to read and 20 for the exercise

What you will learn

  • Design rollout rings with bake times, a control group and an automatic halt for code and content updates.
  • Validate content against the code that will interpret it, and require test coverage of every field.
  • Build rollback that works on hosts that cannot boot or reach the network, without allowing downgrades to vulnerable versions.
  • Map the practices to SSDF 1.1 and the SSDF 1.2 draft's PS.4.

Before you start

Release, SRE and platform engineers who ship agents, clients, rules or configuration to fleets, and the security teams that own detection content

  • Know how your product ships code releases and content or configuration updates, and how clients receive them.
  • Know what a canary deployment is.
  • For the code: Python 3 to run the gate and the rollback drill.

Refresh first: Protect package publication and the update channel

Go to the guided practice

An update moving through rings, with an automatic halt

Release pipelinePromotion gateCanary ringCustomer ringsValidate contentagainst the code1Update to a small, representative ring2Crash, boot andheartbeat signals2Regression: halt androll back3Healthy after bake time: promote3Signals from each later ring4Trust boundary1Step, described below
  1. Validate in CIThe release pipeline builds the update and checks content against the consuming code's interface before anything ships.
  2. CanaryA small ring that mirrors the fleet gets the update; the gate compares its health with a control group for the bake time.
  3. Promote or haltHealthy: the next, larger ring gets it. A regression: distribution stops automatically and the ring rolls back to last-known-good.
  4. Customer ringsBroad rollout reaches customers in the ring they chose; critical systems can wait for the late ring.
Each ring limits how many hosts a bad update can reach before the gate sees it. The halt is automatic; resuming is a human decision.

Why fast security updates need a slow start

Security teams are under pressure to ship fixes and detection content quickly, and that pressure is exactly what turns a bad update into a fleet-wide outage. On 2 July 2019 a new Cloudflare WAF rule with a regular expression that backtracked badly exhausted CPU on every core serving HTTP worldwide; Cloudflare's postmortem says 'The SOP allowed a non-emergency rule change to go globally into production without a staged rollout', while its software releases went through internal, canary and global stages that could take hours or days. On 19 July 2024 a CrowdStrike content update crashed Windows hosts running its sensor; the root cause analysis lists 'Template Instances should have staged deployment' among its findings (/domains/appsec/platform-security/case-study-crowdstrike-channel-file-291/).

In both cases the update was content, not code, the pipeline treated it as safe to push everywhere at once, and the code that interpreted it failed. In both cases the fix was the same: stage content the way code is staged, and keep a separate, still-staged path for emergencies. NIST has since drafted the practice into the Secure Software Development Framework: the SSDF 1.2 initial public draft adds PS.4, 'Ensure Software Updates Are Robust and Reliable'.

Rings, canaries and bake time

Google's SRE Workbook defines canarying as 'a partial and time-limited deployment of a change in a service and its evaluation.' A ring is one step of that: a defined population that gets the update, a bake time during which you watch it, and a gate that decides whether the next ring gets it. Ring membership matters as much as size: a canary must include the operating system versions, hardware, configurations and workloads the fleet has, or it will miss the failure that only appears on one of them.

Compare each ring with a control group that did not get the update, over the same period, rather than with a fixed threshold; otherwise a busy Monday looks like a regression and a quiet Sunday hides one. The Workbook suggests starting from service level indicators and keeping to a small set, 'perhaps no more than a dozen'. For agents and clients, the signals that matter most are crashes, failed boots or automatic rollbacks, hosts that stop sending heartbeats, CPU and memory, and error rates in the product's own telemetry.

Bake time has to cover how the failure would show. A crash at load shows in minutes; a crash at the next reboot only after hosts reboot; a memory leak over a day. Set minimums per ring from your own incident history, and require a minimum number of hosts as well as a minimum time.

Rings, canaries and bake time
RingWhoExample bake timeMoves on when
0: internalYour own staff's machines and test fleets, all supported platformsHoursNo crash, boot or heartbeat regression; functional checks pass
1: canaryA small external slice that mirrors the fleet, including customers who opted in earlyHours, including at least one reboot cycle for agentsNo regression against the control group, with enough hosts reporting
2 and later: broadProgressively larger slicesHours to a day eachSame gate, larger sample
LateSystems customers marked critical, or chose to delayAfter broad rollout is completeCustomer's own schedule

Content updates are code updates

Many products ship two kinds of change: code releases, which go through a long test and rollout process, and content, such as detection rules, signatures, channel or configuration files, feature flags, allow and block lists, or models, which is pushed through a fast path because it changes often and is 'just data'. CrowdStrike described its channel files as 'configuration data; it is not code or a kernel driver.' The file was data; its effect was to choose which array element kernel code read.

The test is what the change can do, not what it is called. If content can make privileged code crash, hang, consume CPU, block traffic or stop protecting the host, give it the same rings and gates as code. Cloudflare's 2019 rule was deployed in 'simulate' mode, which logs instead of blocking, but the rule still ran and still consumed CPU. A feature flag that turns on untested code is a code release with a different name.

Keep a fast path, because security content sometimes has to beat an active attack. Make the fast path faster staging, with shorter bake times and the canary always on, rather than a bypass. Cloudflare's post-incident procedure kept 'the ability to do emergency global deployment for active attacks'; write down who can declare that, and make the global push itself the exception that needs a second person.

Validate content against the code that will run it

Schema validation proves that a file is well formed. It does not prove the consuming code can handle it. CrowdStrike's validator checked new content against a definitions file that said a template had 21 input fields; the sensor code supplied 20. Generate the interface description from the consuming code at build time (field counts, types, ranges, limits), and validate content against that, so the two cannot drift apart.

Run the content through the consumer in CI as well: load each new content file into the real parser or interpreter, built from the release branch, with sanitizers on, and replay representative inputs. Exercise every field with a real value at least once; CrowdStrike's tests used a wildcard in the 21st field throughout, so the missing input was never read. Fuzz the parser that reads content (/domains/appsec/testing/parser-fuzzing/), keep runtime bounds checks in release builds, and for rules that run regular expressions, bound their cost, as the ReDoS guide (/domains/appsec/input-handling/redos-input-validation/) describes.

Sign content as you sign code and have the client verify it before loading, so the fast path is not also an unauthenticated path into privileged code. The trusted publishing guide (/domains/supply-chain/releases/trusted-publishing-and-updates/) covers signing and update metadata.

Automate the promotion gate

The gate below compares a ring with a control group and returns halt, hold or promote. Any boot-failure regression halts at once, whatever the bake time; a crash or missing-heartbeat rate above a multiple of the control rate halts; otherwise the ring holds until both the bake time and the minimum host count are met. The thresholds are examples to tune from your own baseline. Its four unit tests (halt on early boot failures, halt on a crash spike, hold when young, promote after bake time) ran with Python 3.14. Wire 'halt' to stop distribution and start rollback without waiting for a person.

ring_gate.pyPython
"""Decide whether an update may move from one rollout ring to the next.

Compare the ring that has the update with a control group that does not
(same hardware and OS mix, same period). Halt on a boot, crash or heartbeat
regression; hold until the bake time and sample size are met; else promote.
The thresholds are examples: set them from your own fleet's baseline.
"""
from dataclasses import dataclass


@dataclass(frozen=True)
class Health:
    hosts: int  # hosts with the update (or control hosts)
    crashes: int  # crashes or agent restarts in the window
    boot_failures: int  # failed boots or automatic rollbacks
    heartbeats_missing: int  # hosts that stopped reporting


@dataclass(frozen=True)
class Policy:
    min_hours: float = 4.0
    min_hosts: int = 500
    max_crash_ratio: float = 1.5  # ring rate may be at most 1.5x the control rate
    max_missing_ratio: float = 2.0


def rate(count, hosts):
    return count / hosts if hosts else 0.0


def decide(ring: Health, control: Health, hours: float, policy: Policy = Policy()):
    if ring.boot_failures > rate(control.boot_failures, control.hosts) * ring.hosts:
        return "halt", "boot failures above the control group: stop and roll back"
    floor = 1 / max(control.hosts, 1)  # a zero baseline must not hide a spike
    checks = {
        "crash": (ring.crashes, control.crashes, policy.max_crash_ratio),
        "missing heartbeat": (ring.heartbeats_missing, control.heartbeats_missing, policy.max_missing_ratio),
    }
    for name, (ours, theirs, limit) in checks.items():
        ours, theirs = rate(ours, ring.hosts), rate(theirs, control.hosts)
        if ours > max(theirs, floor) * limit:
            return "halt", f"{name} rate {ours:.3%} vs control {theirs:.3%}: stop and roll back"
    if hours < policy.min_hours or ring.hosts < policy.min_hosts:
        return "hold", f"baked {hours:.1f} of {policy.min_hours} h on {ring.hosts} of {policy.min_hosts} hosts"
    return "promote", "healthy for the full bake time"

Kill switches: stop distribution, disable a channel

Have two switches and test both. A distribution halt stops the update service from offering a release or content version to any more hosts; the promotion gate should be able to throw it automatically. A client-side disable turns off one content channel or feature on hosts that already have it, so a bad rule can be neutralized without shipping a new one; it is only useful if the client checks it before loading the content, including early in boot.

A kill switch is also an attack surface: anyone who can throw it can turn off protection across the fleet. Authenticate and sign switch messages like updates, limit who can throw them, log every use, and decide in advance whether a disabled channel fails open or closed. Cloudflare's 2019 postmortem shows the other failure: its responders could not reach the internal control panel because their own access service was down, and used a 'bypass mechanism that wasn't frequently used'. Drill the switch and the access path to it, not just the code.

Rollback that works when the update broke the host

Rolling back at the source helps only hosts that can still fetch the next version. CrowdStrike reverted its bad content 78 minutes after release, but machines already crashing at boot needed someone to delete the file by hand. Design the client so it recovers without the network: keep the last-known-good release or content set on the host, count consecutive failed boots or failed health checks, and fall back automatically. Android's A/B updates are the model for system images: if an update 'fails to boot, the device will reboot back into the old partition and remains usable'. The embedded update guide (/domains/appsec/platform-security/embedded-update-security/) covers A/B slots and boot counters on devices.

Rollback must not reopen a vulnerability. TUF's specification sets the goal that 'An attacker cannot trick clients into installing software that is older than that which the client previously knew'. Keep an anti-rollback floor, the oldest version the client will accept, raise it when you ship a security fix, and make sure the last-known-good copy is always at or above it. The SSDF 1.2 draft asks for both: robust rollback (PS.4.3), with an example to 'Implement protection mechanisms that prevent unauthorized roll-back to vulnerable software versions'.

Rehearse it. Before each promotion, prove on a test host that the rollback target is present, intact and allowed.

Check the rollback target before you promote

This drill reads the release manifest the client trusts and checks that the last-known-good artifact is on disk, matches its recorded SHA-256, and is not below the anti-rollback floor. Run against a correct manifest it prints 'rollback target OK'; against one with a wrong hash and a version below the floor it reports both problems and exits 1. In production the manifest itself must be signed and verified first, as with TUF or your platform's update metadata.

Testcheck_rollback.pyPython
"""Rollback drill: prove the last-known-good release can be restored.

Reads the release manifest the update client trusts and checks that the
last-known-good artifact is on disk, matches its recorded SHA-256, and is not
below the anti-rollback floor (versions with known vulnerabilities that the
client must refuse). Run it on a test host before each ring promotion.
"""
import hashlib
import json
import sys
from pathlib import Path


def version_tuple(v):
    return tuple(int(part) for part in v.split("."))


def check(manifest, root):
    lkg, floor = manifest["last_known_good"], manifest["anti_rollback_floor"]
    path = Path(root) / lkg["file"]
    if not path.is_file():
        return [f"missing last-known-good artifact {path}"]
    problems = []
    digest = hashlib.sha256(path.read_bytes()).hexdigest()
    if digest != lkg["sha256"]:
        problems.append(f"{path.name}: sha256 {digest} does not match the manifest")
    if version_tuple(lkg["version"]) < version_tuple(floor):
        problems.append(f"last-known-good {lkg['version']} is below the anti-rollback floor {floor}")
    return problems


if __name__ == "__main__":
    manifest = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
    problems = check(manifest, sys.argv[2])
    print("\n".join(problems) or "rollback target OK")
    sys.exit(1 if problems else 0)

Let customers choose their ring

Customers know which of their systems they can afford to lose for an hour; you do not. Let them assign hosts to an early, general or delayed ring, and to hold critical systems one version behind, with a cap on how far behind a security fix may lag. The SSDF 1.2 draft's PS.4 asks producers to implement update strategies 'preferably allowing customers to control any updates', and its PS.4.2 examples include letting customers identify systems 'considered safe to receive early updates' and staged rollouts 'with built-in wait times for customer feedback'. CrowdStrike added customer control over where and when its content deploys after July 2024.

Publish what changes and when: release notes for content as well as code, the current ring a version is in, and a status page entry when a rollout halts. Customers who see a halt can hold their own late ring instead of guessing.

Where this sits in SSDF and the other frameworks

SSDF 1.1 (NIST SP 800-218, February 2022) has no practice for how updates are delivered; its testing and release practices cover parts of it. The SSDF 1.2 initial public draft (SP 800-218 Rev. 1, December 2025) adds PS.4 'Ensure Software Updates Are Robust and Reliable', with four tasks: PS.4.1 'Thoroughly test all releases following all guidance in PW'; PS.4.2 'Use tiered update and release strategies, such as canaries, staged roll-out, and test deployments'; PS.4.3 'Include robust rollback mechanisms for updates'; and PS.4.4 'Maintain robust and reliable update engines and associated infrastructure for the delivery of updates'. As of October 2026 it is still a draft; cite it as one, and see the SSDF guide (/frameworks/ssdf-product-lifecycle/) for how the final 1.1 practices map to evidence.

Where this sits in SSDF and the other frameworks
Practice in this guideSSDF 1.1 (final)SSDF 1.2 draft
Validate content against the consuming code; non-wildcard tests per field; fuzz the parserPW.8.2, PO.4.1PS.4.1
Rings, canaries, bake time and an automatic gateNone directly; PO.4.1 for the gate criteriaPS.4.2
Automatic rollback to last-known-good, anti-rollback floorPS.3.1 (archive each release so it can be restored)PS.4.3
Signed releases and content, verified by the clientPS.2.1PS.2.1
Reliable update service with halts and kill switchesNone directlyPS.4.4
Root cause analysis after a bad rollout, applied to the whole classRV.3.3, RV.3.4RV.3.3, RV.3.4, PO.6

Guided practice

You run releases for an endpoint agent on 2 million Windows and macOS hosts. A detection-rule update and an agent code update are both ready. Decide what the pipeline does at each point. Treat the facts as true.

Supplied scenario material
Plain text
Policy: min bake 4 h and 500 hosts per ring; halt if crash rate > 1.5x control, missing heartbeats > 2x control, or any boot-failure regression.
Control group (no update): 10,000 hosts, 20 crashes, 0 boot failures, 30 missing heartbeats in 6 h.
Rule update R-77, canary ring after 1 h: 800 hosts, 1 crash, 0 boot failures, 2 missing heartbeats.
Agent 9.4.0, canary ring after 6 h: 2,000 hosts, 4 crashes, 2 boot failures, 6 missing heartbeats.
Agent 9.4.0 was built from a branch where the rule interpreter supplies 14 fields; R-77's rule type declares 15.
Anti-rollback floor: 9.2.0. Last-known-good on hosts: 9.1.7.
  1. 1. Gate R-77

    What does the gate decide for R-77 at 1 hour?

    Expected result and reasoning

    Hold: there is no regression and 800 hosts meets the 500-host minimum, but only 1 of the 4 required hours has passed.

  2. 2. Gate 9.4.0

    What does the gate decide for agent 9.4.0?

    Expected result and reasoning

    Halt and roll back: 2 boot failures against a control group with none is a boot-failure regression, regardless of the healthy crash and heartbeat rates.

  3. 3. Catch the interface mismatch

    What should CI have done with R-77 and agent 9.4.0?

    Expected result and reasoning

    Failed the content check: the rule type declares 15 fields and the interpreter built from the 9.4.0 branch supplies 14. Fix one side, and block rules that use field 15 until every ring runs code that supplies it.

  4. 4. Check the rollback target

    Is the rollback for 9.4.0 safe?

    Expected result and reasoning

    No: last-known-good 9.1.7 is below the anti-rollback floor 9.2.0, so a correct client would refuse it and hosts would have nothing to fall back to. Ship and verify a last-known-good at or above 9.2.0 before any further promotion.

Your deliverable: The gate decision for each update with its reason, the CI check that would have caught the mismatch, and a corrected rollback plan.

Ring sizes, bake times and thresholds here are examples for the exercise. Real values depend on fleet size, how fast each failure mode shows and your own control-group baselines.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. NIST SP 800-218 Rev. 1 (initial public draft): Secure Software Development Framework version 1.2. Initial public draft, December 2025.
  2. NIST SP 800-218: Secure Software Development Framework version 1.1. Version 1.1 (February 2022).
  3. Google SRE Workbook, chapter 16: Canarying Releases
  4. Cloudflare: Details of the Cloudflare outage on July 2, 2019. 12 July 2019.
  5. CrowdStrike: External Technical Root Cause Analysis, Channel File 291. 6 August 2024.
  6. Android Open Source Project: A/B (seamless) system updates
  7. The Update Framework specification. Version 1.0.37.

Reference information

SDLC phase
Release

Download this guide as Markdown