diff --git a/content/claude-synthid-text-watermark-attack-surface.md b/content/claude-synthid-text-watermark-attack-surface.md index 7ee89c5b..cbb91c33 100644 --- a/content/claude-synthid-text-watermark-attack-surface.md +++ b/content/claude-synthid-text-watermark-attack-surface.md @@ -4,7 +4,7 @@ Author: Oliver Rivas Category: Artificial Intelligence Tags: AI Security, Anthropic, Claude, SynthID, Watermarking, EU AI Act, Adversarial ML Slug: claude-synthid-text-watermark-attack-surface -Description: Claude's SynthID-Text watermark lives in token selection rather than hidden characters, reshaping detection, scrubbing, spoofing, and enterprise misuse. +Description: Anthropic plans to use SynthID-Text during token selection for future Claude models, reshaping detection, scrubbing, spoofing, and misuse. Og_image: images/og/claude-synthid-text-watermark-attack-surface.png Summary: Claude's planned text watermark is embedded through token selection rather than hidden characters. Once enterprises automate on its detector, that provenance feature becomes a security control plane with spoofing, evasion, oracle, key-management, supply-chain, and policy-abuse requirements. @@ -115,7 +115,7 @@ Granular confidence scores reduce the query cost because every response reveals This section is a threat model for Anthropic's forthcoming API. It describes how such an interface could be attacked, and the actual risk will depend on response granularity, threshold stability, authentication, rate limits, similarity clustering, abuse monitoring, and key segmentation. -Calibration poisoning targets any part of the detection pipeline that adapts from prior inputs rather than the query interface itself. The exposure lives in the adaptive layers. A static vendor detector faces little direct risk, while provider-side threshold tuning, an abuse model, or an enterprise allow-and-block policy that learns from a stream of submissions can be steered. An adversary who seeds crafted text over time can shift the decision boundary, suppress a target class of positives, or manufacture false positives against a chosen source. Any feedback loop that tunes on unauthenticated input becomes part of the attack surface and should be trained only on curated, provenance-bound data. +Calibration poisoning targets any part of the detection pipeline that adapts from prior inputs rather than the query interface itself. The exposure lives in the adaptive layers. A static vendor detector faces little direct risk. Adaptive components remain susceptible to poisoning, including provider-side threshold tuning, abuse models, and enterprise allow-and-block policies that learn from streams of submissions. An adversary who seeds crafted text over time can shift the decision boundary, suppress a target class of positives, or manufacture false positives against a chosen source. Any feedback loop that tunes on unauthenticated input becomes part of the attack surface and should be trained only on curated, provenance-bound data. ## Spoofing and Watermark Stealing diff --git a/content/every-alert-is-your-alert.md b/content/every-alert-is-your-alert.md index 23020986..14debac3 100644 --- a/content/every-alert-is-your-alert.md +++ b/content/every-alert-is-your-alert.md @@ -18,7 +18,7 @@ The detection was correct. The traffic was, by every shape the sensor examines, It was also me, deliberately, doing my job. -This post is not about defending myself to a SOC. It is about the gap the detection exposes: every IR runbook I have written or worked from assumes the suspicious transfer came from the adversary or the user. Authorized analyst tooling that mirrors adversary behavior is a third category, and none of them handle it. +In this post I look at the gap the detection exposes: every IR runbook I have written or worked from assumes the suspicious transfer came from the adversary or the user. Authorized analyst tooling that mirrors adversary behavior is a third category, and none of them handle it. ## The setup diff --git a/content/iam-blast-radius-architecture-problem.md b/content/iam-blast-radius-architecture-problem.md index 22334666..f8ef64bf 100644 --- a/content/iam-blast-radius-architecture-problem.md +++ b/content/iam-blast-radius-architecture-problem.md @@ -106,6 +106,6 @@ A clean policy is good. A contained failure domain is better. The strongest IAM programs understand that distinction. They do not stop at removing wildcards. They design systems where one compromised principal cannot rewrite production, erase the evidence, and call it automation. Adoption of that shape is a control in its own right, which is a longer story told in [Adoption Is a Security Control]({filename}paved-road-adoption-as-control.md). -The goal is not a prettier IAM policy. The goal is a system where one compromised principal cannot become a company-wide incident. +The goal is a system where one compromised principal cannot become a company-wide incident. IAM blast radius is an architecture problem. The policy is only where the architecture becomes visible. diff --git a/content/iam-blast-radius-github-action.md b/content/iam-blast-radius-github-action.md index 9f6a9dc0..d48964c4 100644 --- a/content/iam-blast-radius-github-action.md +++ b/content/iam-blast-radius-github-action.md @@ -13,13 +13,13 @@ Summary: A security gate has to say "I don't know" and block on it. The design d Most IAM risk does not arrive as one obviously dangerous policy. It accumulates: the wildcard added "temporarily", the `iam:PassRole` without an `iam:PassedToService` restriction, the trust policy that quietly gains another account. Each change looks reasonable on its own. Combined with the permissions and trust relationships already in place, it can substantially expand what a compromised identity could reach. -The problem is not that engineers cannot read JSON. It is that reviewing individual permissions is not the same as reasoning about their consequences. +Engineers can read JSON. The hard part is understanding the combined consequences of a permission set. Reviewing permissions one at a time does not reveal how they interact. The [IAM Blast Radius analyzer](/tools/iam-blast-radius/) does that reasoning in a browser tab, without sending the policy anywhere. That helps when someone already suspects a policy. It does nothing for the change nobody looked at twice. So the same engine now ships as a GitHub Action on the [GitHub Marketplace](https://github.com/marketplace/actions/iam-blast-radius), and the analysis moves into the pull request. Putting a scanner in CI does not make it a security control. A control needs a defined enforcement boundary, predictable failure behavior, and an explicit way to represent uncertainty. If the engine cannot analyze a policy, it must not report success. If a policy is never scanned, that gap must not be mistaken for a clean result. -So this post is organized around three questions: +So this post answers three questions: 1. What does the engine know? 2. What happens when it does not know? @@ -29,7 +29,7 @@ Then, how I would roll it out. ## What the engine knows -It reports potential blast radius, not effective permissions. It reasons about the policy in front of it, not your whole organization, so it cannot see the SCPs, permissions boundaries or session policies layered on top unless you scan those too. Findings are graded by how much the policy alone supports them. That limitation is stated rather than hidden, because a control that overclaims gets ignored the first time it is wrong. +It reports the potential blast radius of the policy in front of it. SCPs, permissions boundaries, and session policies remain outside that analysis unless scanned separately. Calculating the principal's effective permissions requires evaluating all of those policy layers together. Findings are graded by how much the policy alone supports them. I state that limitation openly, because a control that overclaims gets ignored the first time it is wrong. It recognizes 13 families of privilege escalation, from `iam:PassRole` into compute and policy version tricks to cross-account role assumption and data access through KMS. Version 1.1.0 added one I kept finding in real policies: taking over compute that already holds a role. `ssm:SendCommand`, `ssm:StartSession`, EC2 Instance Connect and presigned SageMaker notebook URLs give code execution on something that already has a role, and that role is now yours. No `iam:PassRole` required. @@ -63,7 +63,7 @@ Three inputs are designed to be inconvenient, because inference here would fail - **`partition` has no default.** An account ID does not tell you whether a role lives in `aws`, `aws-us-gov` or `aws-cn`. Defaulting to `aws` would make cross-partition verdicts look certain when they are assumptions. Leave it unset and any verdict that depends on it fails closed. - **Resource policies need the resource.** Without the ARN of the resource the policy is attached to, the Action does not guess what "this bucket" means. It fails closed. -The principle is the same in all three: when the engine would have to assume something to give an answer, it asks instead, and a missing answer is a failure, not a pass. +The principle is the same in all three: when the engine would have to assume something to give an answer, it asks instead, and treats a missing answer as a failure. A workflow with that context stated: @@ -105,8 +105,8 @@ A policy gate reads content from pull requests, including pull requests from peo - **It reads files and runs nothing.** No Terraform, no npm, no shell, no command from the repository. Policy content stays data. - **No runtime dependencies, no build step.** What is in the tagged commit is what executes. The npm package is published with build provenance through OIDC trusted publishing. -- **Hard ceilings.** Time, file count and total size are bounded, and hitting a bound is a fail-closed exit, not a truncated pass. A pull request cannot make the gate slow, and it cannot make it give up quietly. -- **SARIF is opt-in and separated.** The maintained pattern in [ACTION.md](https://github.com/rivassec/secure-iam-lint/blob/main/ACTION.md) keeps the scan job read-only, gives `security-events: write` only to a separate upload job, publishes the SARIF artifact even when the scan fails, skips uploads for fork pull requests, and never uses `pull_request_target` with an untrusted checkout. That is privilege separation, not a clean room: the SARIF the upload job handles was still derived from untrusted policy files, and it is treated as untrusted data too. +- **Hard ceilings.** Time, file count and total size are bounded, and hitting a bound exits fail-closed, so a truncated scan can never pass. A pull request cannot make the gate slow, and it cannot make it give up quietly. +- **SARIF is opt-in and separated.** The maintained pattern in [ACTION.md](https://github.com/rivassec/secure-iam-lint/blob/main/ACTION.md) keeps the scan job read-only, gives `security-events: write` only to a separate upload job, publishes the SARIF artifact even when the scan fails, skips uploads for fork pull requests, and never uses `pull_request_target` with an untrusted checkout. That is privilege separation with a limit: the SARIF the upload job handles was still derived from untrusted policy files, so it is treated as untrusted data too. ### The CI gate is part of the security boundary @@ -118,7 +118,7 @@ Nobody needs to defeat the analysis engine to merge a risky policy. It is usuall - **Workflow edits.** A pull request lowers `fail-on`, narrows `paths`, or removes the job. - **Unprotected merges.** The check fails correctly and the pull request merges anyway, because nothing requires it to pass. -None of these exploit the scanner. They exploit the pipeline around it. So the gate needs the same treatment as any other control: required in a branch ruleset, workflow file under CODEOWNERS review, an inventory of where every deployable policy comes from with a scan path for each, and alerting on skipped runs and configuration changes, not only on failures. +None of these exploit the scanner. They exploit the pipeline around it. So the gate needs the same treatment as any other control: required in a branch ruleset, workflow file under CODEOWNERS review, an inventory of where every deployable policy comes from with a scan path for each, and alerting on failures, skipped runs, and configuration changes. ## Rolling it out as a control @@ -127,11 +127,11 @@ Turning on a blocking check across an estate on day one is how security tooling 1. **Observe findings, enforce unknowns.** Start with `fail-on: none`. Findings are reported but do not block; fail-closed exits still do, because `none` never turns a `3` into a `0`. The first week tells you two things: how much existing risk you are carrying, and where the engine needs context it does not have yet. 2. **Fix the unknowns first.** Every exit `3` is either missing context (`family`, `partition`, a resource ARN) or input the engine does not support. Resolve them before raising the threshold, or the gate's first impression will be noise. 3. **Ratchet the threshold.** Move to `fail-on: high` once the existing findings are triaged: fixed, or accepted by someone who owns the risk. Lower it later if the signal holds. -4. **Route findings to owners.** The engineer who wrote the policy should see the finding in their pull request, and the person who can accept the risk should own the exception, not the security team by default. +4. **Route findings to owners.** The engineer who wrote the policy should see the finding in their pull request, and the person who can accept the risk should own the exception, even when the security team would be the easier default. ## Where it fits -This does not replace the architecture work. As I argued in [IAM Blast Radius Is an Architecture Problem, Not a Policy Problem](iam-blast-radius-architecture-problem.html), most of the risk is decided before anyone opens a JSON file: account boundaries, trust relationships, which pipeline can reach production. A policy gate cannot fix a flat account structure. +This does not replace the architecture work. As I argued in [IAM Blast Radius Is an Architecture Problem, Not a Policy Problem](iam-blast-radius-architecture-problem.html), architecture determines most of the risk before anyone opens a JSON file: account boundaries, trust relationships, and which pipeline can reach production. A policy gate cannot fix a flat account structure. What it can do is hold the line on drift, in the place where drift happens. The promise is deliberately narrow: an enforceable, auditable check that names potential permission risk during review, and refuses to approve what it could not analyze. It does not certify that a passing policy is safe, or that every risky change gets caught. diff --git a/content/pwnagotchi-plugin-bandit-hardening.md b/content/pwnagotchi-plugin-bandit-hardening.md index 8b7b10a7..e9a13195 100644 --- a/content/pwnagotchi-plugin-bandit-hardening.md +++ b/content/pwnagotchi-plugin-bandit-hardening.md @@ -19,7 +19,7 @@ This is the hardening pass that took [`bt-tether-multi`](https://github.com/riva The patterns generalize. If your Python ever calls a binary, the same checklist applies. -## The threat model is local, not remote +## A local threat model The plugin reads phones from `config.toml` on the same disk Pwnagotchi is running from. There is no network input that becomes a shell argument. So why bother? @@ -37,7 +37,7 @@ Bandit is `pip install bandit` and it ships with rules numbered B602, B603, B607 - **B603**: `subprocess` without an explicit shell, but the binary path comes from a variable rather than a literal. Bandit can't tell whether that variable is trusted. - **B607**: starting a process with a partial executable path. `subprocess.run(["nmcli", "..."])` triggers this because `nmcli` is resolved through `$PATH`, and `$PATH` is something you should not trust on a device whose disk has been out of your sight. -Pyflakes, Ruff, mypy will not flag any of these, because these are policy findings, not type or syntax errors. +Pyflakes, Ruff, mypy will not flag any of these, because these are policy findings, outside what type checkers and linters look for. A clean Bandit run on a hardened plugin looks like this: @@ -146,7 +146,7 @@ def _sanitize_name(self, name): The MAC regex is restrictive on purpose: six pairs of hex separated by colons, nothing else. The name regex allows word characters, spaces, and hyphens, capped at 32 characters. Both of these reject inputs at config-load time so that the rest of the plugin can assume its values are already clean. The validation lives in `_validate_phones()`, which runs from `on_loaded` and `on_config_changed`. Phones that fail validation are dropped, logged, and never reach a subprocess call. -The pattern is "validate at the trust boundary, trust everything past it." The trust boundary in this plugin is the config-load handler. Past that, the rest of the code is allowed to assume `phone["mac"]` matches `^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$`. That assumption is enforced by the validator, not by hope. +The pattern is "validate at the trust boundary, trust everything past it." The trust boundary in this plugin is the config-load handler. Past that, the rest of the code is allowed to assume `phone["mac"]` matches `^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$`. The validator enforces that assumption. ## The `# nosec` discipline @@ -171,7 +171,7 @@ The plugin's repo runs Bandit on push. The relevant fragment of GitHub Actions: `--severity-level low --confidence-level low` is the strictest setting. Anything less and you are filtering out the exact class of finding you want to see. The plugin is small enough that this runs in seconds. The GitHub Actions workflow fails the PR if Bandit returns non-zero, which happens on any unsuppressed finding at any severity. -Pin the Bandit version. Bandit's rule set evolves; an unpinned `bandit` in CI means a future point release can fail your PR for reasons you have not seen before. Bump deliberately, not on every install. +Pin the Bandit version. Bandit's rule set evolves; an unpinned `bandit` in CI means a future point release can fail your PR for reasons you have not seen before. Bump it deliberately, as its own reviewed change. ## The pattern generalizes diff --git a/content/secure-iam-lint-v1-2-0.md b/content/secure-iam-lint-v1-2-0.md index 60962b07..23e90457 100644 --- a/content/secure-iam-lint-v1-2-0.md +++ b/content/secure-iam-lint-v1-2-0.md @@ -32,7 +32,7 @@ This service control policy is meant to keep an account inside two approved regi The third value looks like a harmless pattern added for a future region. It matches every current AWS region name. `StringNotLike` is true only when the requested region matches none of the values, so whenever a request carries a region, this condition is false and the Deny never fires. The fence is decorative. Nothing in this policy keeps requests out of other regions, while it still reads like a control during code review. -The rest of the policy is the usual shape for a region fence. The `NotAction` carve-out is there for global services: IAM, for example, is served from a single endpoint in us-east-1, so without the exemption its calls would be denied whenever us-east-1 sits outside the fence. AWS's own example exempts a longer list of services; the snippet keeps two to stay short, so copy AWS's list, not this one. +The rest of the policy is the usual shape for a region fence. The `NotAction` carve-out is there for global services: IAM, for example, is served from a single endpoint in us-east-1, so without the exemption its calls would be denied whenever us-east-1 sits outside the fence. AWS's own example exempts a longer list of services; the snippet keeps two to stay short, so copy AWS's list into real policies. Version 1.1.0 of [secure-iam-lint](https://github.com/rivassec/secure-iam-lint) called this policy CLEAN with exit `0`. Version 1.2.0 reports it as High and fails the check at the default `fail-on: high`: @@ -99,7 +99,7 @@ Anything the engine does not model still exits `3`, with the JSON path of the st ## New detections -- **DB-AUTH**: `rds-db:connect` is IAM database authentication, which is credential-equivalent. High unless every resource pins a concrete `dbuser:/` (the ARN uses the database or cluster resource ID, not its name). +- **DB-AUTH**: `rds-db:connect` is IAM database authentication, which is credential-equivalent. High unless every resource pins a concrete `dbuser:/` (the ARN uses the database or cluster resource ID; a name in that position matches nothing). - **PASSROLE-UNSCOPED**: `iam:PassRole` on `*` or any all-roles pattern now reports without a companion launch action. It combines with whatever compute permission the holder has elsewhere, and the engine cannot see that from one policy. - **OBJECT-ACL-WRITE**: `s3:PutObjectAcl` and `s3:PutObjectVersionAcl` can make objects public or share them cross-account, unless Object Ownership is `BucketOwnerEnforced` (the default for new buckets, which rejects ACL changes); S3 Block Public Access also stops the public case. Both are bucket or account settings a policy cannot show. High on `bucket/*` or broader, medium on one named object. - **Open federated trusts are Critical.** A GitHub OIDC trust with a missing or wildcard `sub` (`*`, `repo:*`) lets any repository's workflow assume the role. [IAM now refuses](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html#idp_oidc_Create_GitHub) to save one whose `sub` is missing or only a wildcard, so the shapes you will still meet are prefixes like `repo:*` and policies IAM hasn't seen yet, such as infrastructure code before it is applied. A Cognito trust whose `aud` is not pinned to a literal identity pool id, a wildcard for example, admits identity pools in any account through the basic (classic) authentication flow, including one an attacker creates in their own. A Cognito trust with no `aud` at all can no longer be saved, but [per AWS](https://docs.aws.amazon.com/cognito/latest/developerguide/role-trust-and-permissions.html) an older one still admits any identity pool in the role's own account, and the tool rates it Critical as well. These now fail even at `fail-on: critical` (`--threshold critical` in the CLI). diff --git a/content/telemetry-turns-predatory.md b/content/telemetry-turns-predatory.md index 3fd6356a..0cc778b6 100644 --- a/content/telemetry-turns-predatory.md +++ b/content/telemetry-turns-predatory.md @@ -54,7 +54,7 @@ EFF's [*Unveiling Venezuela's Repression: Surveillance and Censorship Following **Engineering interpretation** -Viewed as a system, VenApp is a citizen-scale SIEM. The ingest layer is a mobile client; the events are user-attributed reports of "suspicious" behavior; the natural entity-resolution layer for those reports is the state identity infrastructure EFF documents as weaponized against the population - the Carnet de la Patria and Sistema Patria apparatus - joining a report to a person through national-ID and phone records the state already holds, the same shape as an enterprise correlation against email, SSO subject, and device ID. Any engineer who has built a Splunk ingest pipeline, an Elastic detection rule, or a Sumo Logic content pack recognizes the pattern immediately. Normalization, field extraction, correlation, alert scoring, case creation - these are the same stages that turn raw logs into an IR ticket for a credential-stuffing attempt in an enterprise SOC. The difference is not architectural. The difference is who the tenant is, what the tenant treats as a "threat," and what happens at the response stage. In one pipeline the response is an account lockout and a support ticket. In the other, the response is physical. +Viewed as a system, VenApp is a citizen-scale SIEM. The ingest layer is a mobile client; the events are user-attributed reports of "suspicious" behavior; the natural entity-resolution layer for those reports is the state identity infrastructure EFF documents as weaponized against the population - the Carnet de la Patria and Sistema Patria apparatus - joining a report to a person through national-ID and phone records the state already holds, the same shape as an enterprise correlation against email, SSO subject, and device ID. Any engineer who has built a Splunk ingest pipeline, an Elastic detection rule, or a Sumo Logic content pack recognizes the pattern immediately. Normalization, field extraction, correlation, alert scoring, case creation - these are the same stages that turn raw logs into an IR ticket for a credential-stuffing attempt in an enterprise SOC. The architecture is the same. What differs is who the tenant is, what the tenant treats as a "threat," and what happens at the response stage. In one pipeline the response is an account lockout and a support ticket. In the other, the response is physical. ### Freedom House @@ -64,7 +64,7 @@ Viewed as a system, VenApp is a citizen-scale SIEM. The ingest layer is a mobile **Engineering interpretation** -Operación Tun-Tun is what happens when endpoint detection and kinetic response merge into a single workflow. In an enterprise EDR context - Falcon, SentinelOne, Defender - an alert produces a ticket, a triage step, and typically at worst a device quarantine. The kinetic parallel exists (physical security escorting a compromised laptop off-site) but it is rare and tightly scoped. In the Venezuelan case, the same alert-to-response loop is compressed and applied at population scale: an online post, a phone search at a Tun-Tun checkpoint of the kind Human Rights Watch documents, and an arrest can function as stages of a single pipeline whose escalation path is physical. From a threat-modeling perspective, the transformation is not "surveillance was added." The transformation is that the response stage of an ordinary detection pipeline was rewired to enforcement rather than remediation. +Operación Tun-Tun is what happens when endpoint detection and kinetic response merge into a single workflow. In an enterprise EDR context - Falcon, SentinelOne, Defender - an alert produces a ticket, a triage step, and typically at worst a device quarantine. The kinetic parallel exists (physical security escorting a compromised laptop off-site) but it is rare and tightly scoped. In the Venezuelan case, the same alert-to-response loop is compressed and applied at population scale: an online post, a phone search at a Tun-Tun checkpoint of the kind Human Rights Watch documents, and an arrest can function as stages of a single pipeline whose escalation path is physical. From a threat-modeling perspective, the response stage of an ordinary detection pipeline was rewired from remediation to enforcement. The primitives were already in place. @@ -99,7 +99,7 @@ The value of the Venezuelan case study for a security engineer is that it expose **1. Add a Weaponization section to every design review.** -Standard threat modeling frameworks largely assume an adversary attacking from outside the system. Even LINDDUN, which treats the operator's own data processing as a privacy threat source, does not model the case where the legitimate operator turns the full detection-and-response pipeline against its subjects. Add a fourth question to every design doc: *If an authoritarian tenant owned this pipeline, what would they do with it?* For every data flow you draw, write one sentence describing the weaponized use case. The exercise is uncomfortable, which is the point. +Standard threat-modeling exercises often focus on adversaries attacking from outside the system. Even LINDDUN, which treats the operator's own data processing as a privacy threat source, does not model the case where the legitimate operator turns the full detection-and-response pipeline against its subjects. Add a fourth question to every design doc: *If an authoritarian tenant owned this pipeline, what would they do with it?* For every data flow you draw, write one sentence describing the weaponized use case. The exercise is uncomfortable, which is the point. A minimal template lives at [github.com/rivassec/weaponization-threat-model](https://github.com/rivassec/weaponization-threat-model). It provides a one-page addendum to a standard STRIDE doc: for each data element, the authoritarian-tenant reading; for each retention decision, the subpoena-inversion reading; for each correlation-key choice, the population-tracking reading. Adopt it, fork it, or replace it. The specific artifact matters less than the commitment to run the exercise. @@ -109,13 +109,13 @@ Encryption of message bodies is now table stakes. Metadata - who talked to whom, **3. Design as if governance will change.** -The tools you build outlive the leadership that scoped them. The privacy commitments made by a founding team cannot be relied on to bind the acquirer, the successor board, or the government that subpoenas the resulting system. Retention sunsets should be the default, not the exception. Owner-change reviews - what happens to this pipeline if the executive who scoped it leaves, if the company is acquired, or if a nation-state compels access - should be a scheduled part of your design cadence. This one I have practiced as a habit but not yet built into tooling; I raise it in design reviews and it lands or it doesn't, depending on the room. It should be built into the process instead. +The tools you build outlive the leadership that scoped them. As governance changes, a founding team's privacy commitments become unreliable constraints on a future acquirer, a successor board, or a government seeking access to the resulting system. Make retention sunsets the default. Owner-change reviews - what happens to this pipeline if the executive who scoped it leaves, if the company is acquired, or if a nation-state compels access - should be a scheduled part of your design cadence. This one I have practiced as a habit but not yet built into tooling; I raise it in design reviews and it lands or it doesn't, depending on the room. It should be built into the process instead. -I write "should be" because I have seen the shape of what happens when it isn't. Some years ago, I was part of a rollout at a large communications platform of a feature that required users to verify a phone number to keep certain account functions. I raised the abuse concern internally: in jurisdictions where pseudonymous accounts were the only safe way for activists to organize, a phone-verification requirement was going to be a state's dream - a real-name join key against every account operating under a handle. The escalation went to the appropriate teams. It was denied. Later, activists in one of the affected jurisdictions were arrested, and some did not survive their detention. I did not sleep well for a long time after that decision was made. I know correlation is not causation, and I do not know whether the verification requirement contributed to those arrests - I am not claiming it did. I include this because the shape of the harm is what security engineers need to sit with, not because I claim to know its exact mechanism. It informs how I write every design review now. +I write "should be" because I have seen the shape of what happens when it isn't. Some years ago, I was part of a rollout at a large communications platform of a feature that required users to verify a phone number to keep certain account functions. I raised the abuse concern internally: in jurisdictions where pseudonymous accounts were the only safe way for activists to organize, a phone-verification requirement was going to be a state's dream - a real-name join key against every account operating under a handle. The escalation went to the appropriate teams. It was denied. Later, activists in one of the affected jurisdictions were arrested, and some did not survive their detention. I did not sleep well for a long time after that decision was made. I know correlation is not causation, and I do not know whether the verification requirement contributed to those arrests - I am not claiming it did. I include it because security engineers need to sit with the shape of the harm, even without knowing its exact mechanism. It informs how I write every design review now. ## Caveats -Internet censorship measurement is inherently difficult, and the sharpest limitation in this analysis is attribution. The network measurements demonstrate that DNS, IP, and TLS-layer interference occurred, but they do not by themselves prove central coordination versus independent per-ISP compliance. The internal architecture of systems like VenApp is inferred here from observable behavior, not from documented internals, and I have flagged those inferences as interpretation rather than fact throughout. +Internet censorship measurement is inherently difficult, and the sharpest limitation in this analysis is attribution. The network measurements demonstrate that DNS, IP, and TLS-layer interference occurred, but they do not by themselves prove central coordination versus independent per-ISP compliance. I infer the internal architecture of systems like VenApp from observable behavior alone, and I flag each of those inferences as interpretation throughout. ## References