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.
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.com —
failed 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.
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.
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
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 thingIf 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.
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.
bash, pwsh or run_code callThis 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.
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.
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.
- 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.
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.
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”.
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.
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.
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.
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.
*.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.
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.
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.
deny: ['*.s3.amazonaws.com'] still compiles. A deny that is wider than its
author meant refuses more, which is the safe direction.mybucket.s3.amazonaws.com was accepted and still is.policyFile. It could never widen anything and still cannot: it has no
allowWideWildcards key, and a file carrying one is invalid in whole.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.
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.