Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions content/claude-synthid-text-watermark-attack-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion content/every-alert-is-your-alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion content/iam-blast-radius-architecture-problem.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 9 additions & 9 deletions content/iam-blast-radius-github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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.

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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.

Expand Down
8 changes: 4 additions & 4 deletions content/pwnagotchi-plugin-bandit-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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:

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading
Loading