Egress & namespace policy¶
Translation rules (previous page) decide what the proxy may substitute. Egress and namespace policy decide where it may send the result.
There are two layers:
| Layer | Scope | Surface |
|---|---|---|
| Egress allow/block | Global, per-domain | Policy → Egress |
| Namespace egress | Per-namespace, hierarchical | Fleet & Namespaces + Policy → Egress |
Both layers are evaluated on every CONNECT. Both fail closed in enforcement mode: a domain that no layer allows is refused before TLS handshake.
Layer 1 — Egress allow/block¶
The global egress layer controls which destination domains are permitted or blocked across the fleet.
Where in the UI¶
Policy → Egress.
The page has three regions:
- A
domain (e.g. api.stripe.com)input with Allow and Block buttons. - An Allowed table with columns Domain, Reason, Auth, Remove.
- A Blocked table with the same columns.
Adding an allow¶
In the UI, type the domain, optionally choose require-auth, click Allow.
By API:
curl -X POST http://localhost:8443/api/policy/egress/allow \
-H "Content-Type: application/json" \
-d '{
"domain": "api.stripe.com",
"reason": "payments",
"require_auth": true
}'
Multiple domains, common patterns:
for d in api.stripe.com api.github.com api.bank.eu \
oauth2.googleapis.com s3.eu-west-1.amazonaws.com; do
curl -X POST http://localhost:8443/api/policy/egress/allow \
-d "{\"domain\":\"$d\",\"reason\":\"prod\",\"require_auth\":true}"
done
Blocking¶
curl -X POST http://localhost:8443/api/policy/egress/block \
-d '{"domain":"evil-c2.example","reason":"threat-intel"}'
A blocked domain wins over an allowed one with the same exact match.
Removing¶
curl -X DELETE 'http://localhost:8443/api/policy/egress/allow?domain=api.stripe.com'
Or click Remove in the row.
What's in Reason for?¶
Just an audit string. It is recorded with every domain allow/block event and is invaluable in incident review later.
Layer 2 — Namespace egress (hierarchical)¶
Workloads can be grouped into hierarchical namespaces like
prod, prod/payments, prod/payments/eu-west. Each namespace
can carry its own allowed_domains list.
The fundamental invariant:
Children may only narrow. A child namespace can shrink the parent's allow-list. It can never widen it.
Why this matters¶
It means an operator who runs a sub-team's namespace cannot
escalate beyond the parent's grants. A platform team that gives
prod/payments permission for api.stripe.com and api.bank.eu
knows that no descendant of prod/payments will ever reach
anywhere those two domains are not in its own ancestry.
Where in the UI¶
Fleet & Namespaces:
The Namespaces table shows columns Name, Parent, Depth, Allowed domains. The Workloads table shows which workloads belong to which namespace.
Creating a namespace¶
curl -X POST http://localhost:8443/api/fleet/namespaces \
-H "Content-Type: application/json" \
-d '{
"name": "prod/payments",
"description": "payment-service workloads",
"allowed_domains": ["api.stripe.com","api.bank.eu"]
}'
Creating a child that narrows further¶
curl -X POST http://localhost:8443/api/fleet/namespaces \
-H "Content-Type: application/json" \
-d '{
"name": "prod/payments/eu-west",
"description": "EU-only payments cluster",
"allowed_domains": ["api.bank.eu"]
}'
This is valid: every child domain (api.bank.eu) is covered
by the parent's allow-list. The child cannot reach
api.stripe.com even though the parent can.
Trying to widen¶
curl -X POST http://localhost:8443/api/fleet/namespaces \
-d '{
"name": "prod/payments/eu-west",
"allowed_domains": ["api.stripe.com","api.bank.eu","api.foo.com"]
}'
This is rejected:
{
"error": "child_widens_parent",
"namespace": "prod/payments/eu-west",
"domain": "api.foo.com",
"ancestor": "prod/payments",
"message": "domain api.foo.com is not covered by ancestor prod/payments"
}
Updating a parent that would break a child¶
If you change a parent's allowed_domains in a way that would
make a descendant non-compliant, the update is rejected by default
with structured cascade_warnings:
{
"error": "cascade_violations",
"cascade_warnings": [
{
"child": "prod/payments/eu-west",
"uncovered_domains": ["api.bank.eu"]
}
]
}
You can force the update with ?force=true. Doing so applies the
change and returns the warnings; the descendants are now broken
until you fix them. Use this only when you intend to also fix the
children atomically.
Evaluation at request time¶
For every CONNECT:
- Walk from the workload's namespace leaf to root.
- At every namespace that defines
allowed_domains, the target host must match. - A
blocked_domainsentry at any level overrides any allow. - If no namespace in the chain defines
allowed_domains, the root's default action applies. - If an intermediate namespace is missing, it is skipped (a warning is logged) and the walk continues.
This means a workload in prod/payments/eu-west calling
api.stripe.com is denied because the leaf does not allow it,
even though the parent does.
What gets recorded¶
| Event | When |
|---|---|
policy_allow |
Domain explicitly added to the allow-list |
policy_block |
Domain explicitly blocked |
policy_removed |
Domain removed from either list |
proxy_domain_denied |
Per-request denial (egress + namespace) |
namespace_created |
Namespace created (including the allowed-domain set) |
namespace_updated |
Namespace modified (records the cascade warnings) |
Look for proxy_domain_denied on the Audit page when
diagnosing "why was my request blocked."
Common operational patterns¶
These patterns show typical namespace hierarchies in production.
Per-environment narrowing¶
/ (default-deny)
└── prod ["api.stripe.com","api.github.com","api.bank.eu"]
├── prod/payments ["api.stripe.com","api.bank.eu"]
└── prod/builds ["api.github.com","registry.npmjs.org"]
prod/builds references registry.npmjs.org — if it is not in
prod, the create call is rejected. Add it to prod first.
Quarantining a namespace¶
curl -X POST http://localhost:8443/api/fleet/namespaces \
-d '{
"name": "prod/payments",
"allowed_domains": []
}'
An empty allowed_domains denies everything. Combined with
?force=true this is the fastest way to cut a namespace off the
internet without revoking individual workload tokens.
Investigating a deny¶
A denied request shows up on the Workstation page in the
Suspicious activity table with reason
proxy_domain_denied, and on the Audit page as a row tagged
proxy_domain_denied. The latter quotes both the target host and
the deciding policy layer.
Related¶
- Translation rules — gates the credentials, not the destinations.
- Responding to an incident — the Audit / Threat / Workstation pages all consume these policy decisions.

