dsh-netguard

Upgrade notes

← dsh-netguard docs

0.7.0 relaxes the wide-wildcard refusal, and this one is a relaxation

Read this even if your profile boots. Every other note on this page describes a control getting tighter. This one describes a control that stopped failing the boot, and if you were relying on that failure you have to ask for it back by hand.

What stopped being enforced

Through 0.5.0 and 0.6.0, an allow: entry wildcarding a namespace where a third party can register the name — *.s3.amazonaws.com, *.slack.com, *.co.ke, **.githubusercontent.comfailed the load, and the only way to run one was to name the namespace in allowWideWildcards. From 0.7.0 that same entry compiles and works by default. It is reported, not refused.

Concretely, a profile that failed to boot on 0.6.0 now starts, and the grant it carries is exactly as wide as it always read: *.s3.amazonaws.com permits every bucket anyone can create.

Why

The maintainer’s call, and the reasoning is worth stating because it is a security decision: compared with no allow list at all — which is what an operator falls back to when a one-line entry fails the boot — *.s3.amazonaws.com is still a very large narrowing, and it is a simple thing to write. Refusing it outright pushed some deployments to allow: ['*'] or to no netguard at all. So the entry is admitted, and the cost of it is made loud and countable instead.

What you get instead of the refusal

At every boot, one advisory per unreviewed wide entry, on ctx.logger.warn and on stderr (wrapped here for the page):

dsh-netguard: allow entry '*.s3.amazonaws.com' is a wildcard over "s3.amazonaws.com", a namespace
anyone can register a name under. It is permitted and every decision it clears is recorded as one
a wide entry cleared.
  Why: the Public Suffix List's private section carries "s3.amazonaws.com", which is its owner
declaring that the name one label under it belongs to whoever registered it.
  From: Public Suffix List VERSION 2026-09-03_19-51-30_UTC, retrieved 2026-09-05.
  Narrower: '*.<your-name>.s3.amazonaws.com', or '<your-name>.s3.amazonaws.com' for that one host.
  To record this namespace as reviewed and stop this message, add it to the dsh-netguard row of
the profile's cordis.yml or cordis.patch.yml:

  - id: dsh-netguard
    config:
      allowWideWildcards: ['s3.amazonaws.com']

  To fail the load on an entry like this instead, set wideWildcards: 'refuse' on the same row.

And in the spool, on every decision such an entry clears, whether or not you silenced the advisory:

{"unmapped":{"dsh":{"verdict":"allowed","rule":"allow:*.s3.amazonaws.com","wide_wildcard":"s3.amazonaws.com"}}}

dsh-netguard report totals them:

permitted by a wide wildcard entry: 314
  a wildcard over a namespace anyone can register a name under; see docs/enforcement.md

by wide wildcard namespace
     314  s3.amazonaws.com

To keep the 0.6.0 behaviour

One line, on the deployment’s own cordis.yml or cordis.patch.yml row:

- id: dsh-netguard
  config:
    wideWildcards: refuse

That restores the load-time failure exactly, including the message and both remedies it quotes. A repo-local policyFile may not set it, in either direction: warn there would be a widening, and refuse there would let a hostile workspace fail the boot of a deployment that meant its entry.

allowWideWildcards still means one thing

If your profile already names namespaces there, nothing changes for them: they compile under either posture and produce no advisory, exactly as before. The list has one meaning in both postures — this namespace has been reviewed and accepted. Under refuse that review is what makes the entry compile; under warn it is what stops the message. It never changes what the entry grants, and it never removes wide_wildcard from the records.

The list of wide namespaces grew a lot

0.7.0 vendors the whole Public Suffix List, both sections — 6,949 ICANN rules and 3,372 private-section rules, of which the 8,865 with two labels or more are carried — in place of the 2,806-entry selection 0.6.0 shipped. Nothing is fetched at build or run time; src/suffixes.ts records the VERSION and COMMIT it came from.

So more shapes are wide than before: 6,162 more bases, including every geographic second level a registry runs (*.oslo.no, *.ny.us, *.aichi.jp, *.k12.ak.us) and 400 names that are wide only because a listed namespace sits below them (*.salesforce.com, *.linode.com, *.render.com, *.fastly.net, *.rit.edu). Under the default posture each of those is one advisory line. Under wideWildcards: refuse each of them is a boot failure. A profile that sets refuse should therefore start once on this release before deploying it: the plugin tree loads at boot, so a newly wide entry fails there and the message names it.

Entries scoped to a tenant or to a name you hold did not move: *.mycompany.atlassian.net, myteam.slack.com, *.mybucket.s3.amazonaws.com, *.mycompany.co.ke and ordinary corporate names all compile with nothing said about them, as they did in 0.6.0.

One more thing the whole list fixes: the Public Suffix List’s eight exception rules are now honoured, so *.city.kawasaki.jp and **.www.ck — names the list explicitly hands to a single holder — compile quietly where 0.6.0 flagged the first and had no view of the second.

The command arm can now refuse a bash, pwsh or run_code call

This is the one upgrade that can start refusing work an enforce-mode profile was doing. No boot fails, no configuration is rejected, and audit-mode profiles are unaffected except that they record more. What changes is that under mode: enforce, a command whose text names a host your allow list does not cover is now refused before the child process starts, and the model is told why.

What it looks like

dsh-netguard refused this request to registry.npmjs.org: blocked-by-allowlist. Ask the user to
add the host to netguard's allow list if this request is expected.

What to do first

Run one day in mode: audit before switching, then read what the arm saw:

dsh-netguard report --suggest

The suggestion block now includes hosts read out of commands, not only hosts that were fetched. A host in it that you did not expect is exactly what this arm exists to show you.

If you would rather not have it

- id: dsh-netguard
  config:
    shell:
      enabled: false

That restores the previous behaviour exactly: no command is read, no command is refused, and no command produces a record.

What it does not change

A command that does not write its destination out is unaffected — npm ci, make, curl "$(cat url.txt)". Only a URL written with a scheme is read unless you set shell.readTextHosts: true, so a command mentioning main.cc or socket.io is not refused.

Twenty more namespaces are refused, on netguard’s own evidence

This upgrade fails one boot for any profile whose allow: list wildcards one of these:

app.github.dev   atlassian.net    freshdesk.com    glitch.me        itch.io
koyeb.app        lhr.life         livejournal.com  modal.run        neocities.org
slack.com        sourceforge.io   squarespace.com  substack.com     surge.sh
tumblr.com       tunnelto.dev     weebly.com       wordpress.com    zendesk.com

*.slack.com on an allow list is every Slack workspace on the internet, *.atlassian.net is every Jira and Confluence tenant, and *.zendesk.com is every help centre. Each reads like a grant to one vendor.

These are not on the Public Suffix List, in either section, so the tables in the previous two sections could never refuse them. That list is populated by request and these owners never asked. netguard carries them on a test it ran and records: a label nobody has claimed resolves under the name, and the operator answers that hostname saying no tenant holds it — livejournal.com replies “The journal … is not currently registered”, freshdesk.com replies “You can claim it now”.

What to write instead

The remedies are the two the next section already describes, unchanged. Name your own tenant:

      allow:
        - 'mycompany.atlassian.net'          # your site, and nothing else on the namespace
        - '*.mycompany.atlassian.net'        # names under it, if you use them

or open the namespace by name, which is deployment configuration a policyFile cannot set:

      allowWideWildcards: ['atlassian.net']

An entry with a label of your own between the wildcard and the namespace was never affected: *.mycompany.atlassian.net, myteam.slack.com and mycodespace-8080.app.github.dev all compile as they did.

One documented entry changed with it

docs/enforcement.md listed **.githubusercontent.com in its starting allow list. That has been refused since 0.5.0 — githubusercontent.com is a namespace GitHub hands to every user — and the document now names raw.githubusercontent.com and objects.githubusercontent.com instead. If you copied that row, this is the boot it fails.

Wildcards over self-service namespaces are refused at load

This note describes 0.5.0. Since 0.7.0 the refusal is an advisory unless the deployment sets wideWildcards: refuse; read the 0.7.0 note at the top of this page first. Everything below still describes what refuse does, and both remedies still work under either posture.

This upgrade fails one boot for any profile whose allow: list wildcards a namespace where a third party can register the name the wildcard would match. Nothing is silently narrowed; the profile does not start until the entry is fixed.

What it looks like

Error: dsh: plugin tree failed to load: failed to apply loader entry include (cordis:include):
failed to apply loader entry dsh-netguard (dsh-netguard): dsh-netguard policy:
"*.s3.amazonaws.com" is a wildcard over "s3.amazonaws.com", a namespace anyone can register a
name under, so the entry allows every name an attacker can create there. Narrow it to the name
you use, or open the namespace by name. Both go on the dsh-netguard row of the profile's
cordis.yml or cordis.patch.yml:

  - id: dsh-netguard
    config:
      allow:
        - '*.<your-name>.s3.amazonaws.com'   # or '<your-name>.s3.amazonaws.com' for that one host

  - id: dsh-netguard
    config:
      allowWideWildcards: ['s3.amazonaws.com']

  allowWideWildcards is deployment configuration only: a repo-local policyFile may tighten,
  never open one.

The message names the entry that failed and both remedies. If you are reading this at 2am, the line you want is one of the two blocks it quotes.

Why

*.s3.amazonaws.com reads like a grant to one vendor. It matches every bucket anyone can create, including one an attacker registers while your incident is open. The same is true of *.pages.dev, *.workers.dev, *.vercel.app, *.githubusercontent.com, *.blob.core.windows.net, *.github.io, *.herokuapp.com, *.netlify.app, *.azurewebsites.net, *.appspot.com, *.web.app, *.run.app, *.cloudfront.net, *.r2.dev, *.hf.space and around 1,400 more — and of *.co.ke, *.com.pk and every other country registry’s own second level, which the next section covers. Releases up to 0.4.0 accepted all of them.

Remedy 1 — narrow the pattern

Almost always the right one. You do not need every bucket on s3.amazonaws.com; you need yours.

# before — every bucket anyone can create
- id: dsh-netguard
  config:
    allow: ['*.s3.amazonaws.com']

# after — the names under one bucket you own
- id: dsh-netguard
  config:
    allow: ['*.mybucket.s3.amazonaws.com']

A wildcard over a name inside the namespace is accepted, and so is the exact host: mybucket.s3.amazonaws.com on its own grants that host and nothing else. For a namespace the Public Suffix List states as a wildcard rule — *.compute.amazonaws.com, *.rds.amazonaws.com, *.run.app — the name one label under it is still a namespace, so narrow two labels in: *.mydb.abc123.eu-west-1.rds.amazonaws.com.

Remedy 2 — open the namespace by name

When you really do mean every name under it — a build fleet that reaches many of your own buckets, a vendor whose per-tenant subdomain you cannot enumerate — name the namespace and the entry compiles:

- id: dsh-netguard
  config:
    allow: ['*.s3.amazonaws.com']
    allowWideWildcards: ['s3.amazonaws.com']

The entry is the namespace, not a pattern: one line opens both the *. and the **. spelling of it, and allowWideWildcards: ['*.s3.amazonaws.com'] is itself a load-time error. It opens that namespace only — allowWideWildcards: ['pages.dev'] does nothing for *.workers.dev. A top-level domain cannot be opened at all; * in the allow list is the entry that means every host.

This is the shape allowPrivateAddresses already has, and it is what makes an incomplete table safe: a namespace this package never listed is one line away from working.

What does not change

Finding out before you deploy

Every affected entry starts with *. or **., and YAML makes them easy to find: a bare * opens an alias, so every one of them is quoted.

grep -rnE "['\"]\*{1,2}\." "$DSH_HOME/profiles/<name>/"

An entry that survives has at least one label of your own between the wildcard and the vendor namespace. To check a profile without disturbing a live one, copy it, upgrade the copy, and run any short task against the copy: the refusal happens while the plugin tree loads, before the agent starts, and dsh exits 1 with the message above on stderr.

Wildcards over a multi-part public suffix are refused too

Same error, same two remedies, different reason: co.ke is not a company, it is where Kenya’s registry starts handing out names. *.co.uk and about forty others already failed the boot in 0.4.0; this release completes the table for every country-code top-level domain, so *.co.ke, *.com.pk, *.com.ng, *.com.eg, *.gob.mx, *.or.ke and roughly 1,300 more now fail it as well.

# before — every company registered in Kenya
allow: ['*.co.ke']

# after — the names under one of yours
allow: ['*.mycompany.co.ke']

# or, deliberately, all of them
allow: ['*.co.ke']
allowWideWildcards: ['co.ke']

The exact host was accepted before and still is: mycompany.co.ke grants that host alone.

What the table does not carry is a registry’s geographic second levels — oslo.no, aichi.jp, ny.us, roma.it — and the structure under them. Those are public suffixes and a wildcard over one is genuinely wide, but they are not what an egress allowlist gets written over, so they still compile. If yours is one of them, close it with a deny entry.