netbird-zero-trust.md: new doc for the zero-trust mesh overlay
New file covering NetBird - a self-hosted WireGuard mesh giving every fleet host and personal device a stable overlay identity, with a genuinely-implemented zero-trust policy layer (deny-by-default, groups, scoped setup keys, peer approval, posture checks). - Why a mesh over a classic VPN concentrator, illustrated with a real topology diagram (control plane handles coordination only; actual data flows peer-to-peer, bypassing it entirely - the literal distinction from a concentrator, drawn not just asserted) - Ties back to today's iac-pipeline.md update: this LXC is the first host ever built through the new Terraform LXC-native path, start to finish, no manual provisioning step - Real bring-up incident: gRPC/h2c requirements broke through a naive proxy chain, and a proxying CDN 403s the agent outright - both root causes documented, not just the fixes - Evidence: 3 real command blocks (docker ps, live Caddyfile splitter config, netbird status --detail from an enrolled peer), 2 real screenshots (dashboard Peers list, Access Control Policies - port counts in the screenshot match the real policy detail in the text) - Second diagram (edge routing chain) for the specific gRPC/h2c/path split detail, kept separate from the topology diagram since they show genuinely different shapes of information - Honest, non-rounded-up rollout status table - several phases still explicitly open - Sanitize-checked: no internal IPs/domains/personal email Also: fixes iac-pipeline.md's forward-reference to this doc to use the repo's actual link convention ([docs/x.md](x.md)), not descriptive prose link text.
This commit is contained in:
1 parent
2d6be37271
commit
8ceadab38a
6 files changed
+148
-1
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Do not edit this file with editors other than draw.io -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="background: transparent; background-color: transparent; color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1062px" height="248px" viewBox="0 0 1062 248" content="<mxfile host="embed.diagrams.net" scale="1" border="0"> <diagram name="Edge Path" id="netbird-edgepath-1"> <mxGraphModel dx="1414" dy="791" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1080" pageHeight="320" math="0" shadow="0"> <root> <mxCell id="0" /> <mxCell id="1" parent="0" /> <mxCell id="client" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=13;fontStyle=1;shadow=1;" value="Client&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;gRPC agent&lt;/font&gt;" vertex="1"> <mxGeometry height="70" width="180" x="20" y="110" as="geometry" /> </mxCell> <mxCell id="edge" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontColor=#333333;fontSize=13;fontStyle=1;shadow=1;" value="Edge firewall&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;HTTPS termination&lt;/font&gt;" vertex="1"> <mxGeometry height="70" width="200" x="280" y="110" as="geometry" /> </mxCell> <mxCell id="proxy" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontColor=#333333;fontSize=13;fontStyle=1;shadow=1;" value="In-stack proxy&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;content-type / path split&lt;/font&gt;" vertex="1"> <mxGeometry height="70" width="220" x="560" y="110" as="geometry" /> </mxCell> <mxCell id="server" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;" value="Management / Signal&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;h2c end-to-end&lt;/font&gt;" vertex="1"> <mxGeometry height="65" width="220" x="850" y="30" as="geometry" /> </mxCell> <mxCell id="dash" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;" value="Dashboard SPA" vertex="1"> <mxGeometry height="65" width="220" x="850" y="200" as="geometry" /> </mxCell> <mxCell id="e1" edge="1" parent="1" source="client" style="html=1;endArrow=block;strokeColor=#6c8ebf;strokeWidth=2;rounded=1;" target="edge"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="e2" edge="1" parent="1" source="edge" style="html=1;endArrow=block;strokeColor=#d79b00;strokeWidth=2;rounded=1;" target="proxy"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="e3" edge="1" parent="1" source="proxy" style="html=1;labelBackgroundColor=#1C1D1E;endArrow=block;strokeColor=#82b366;fontColor=#82b366;strokeWidth=2;rounded=1;fontSize=11;exitX=1;exitY=0.3;entryX=0;entryY=0.5;" target="server" value="&lt;span style=&quot;background-color: light-dark(rgb(255, 255, 255), rgb(28, 29, 30));&quot;&gt;gRPC / API / relay&lt;/span&gt;"> <mxGeometry relative="1" x="0.3028" y="51" as="geometry"> <mxPoint as="offset" /> </mxGeometry> </mxCell> <mxCell id="e4" edge="1" parent="1"Line truncated
|
||||||
|
After Width: | Height: | Size: 147 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Do not edit this file with editors other than draw.io -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="background: transparent; background-color: transparent; color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1045px" height="548px" viewBox="0 0 1045 548" content="<mxfile host="embed.diagrams.net" scale="1" border="0"> <diagram name="Mesh Topology" id="netbird-mesh-1"> <mxGraphModel dx="1178" dy="659" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1080" pageHeight="620" math="0" shadow="0"> <root> <mxCell id="0" /> <mxCell id="1" parent="0" /> <mxCell id="cp" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontColor=#333333;fontSize=13;fontStyle=1;shadow=1;" value="NetBird control plane&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;Management + Signal + Relay + IdP&lt;br&gt;coordination only — not in the data path&lt;/font&gt;" vertex="1"> <mxGeometry height="90" width="280" x="400" y="260" as="geometry" /> </mxCell> <mxCell id="p1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;" value="Fleet host&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;e.g. Vault&lt;/font&gt;" vertex="1"> <mxGeometry height="65" width="190" x="60" y="30" as="geometry" /> </mxCell> <mxCell id="p2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;" value="Fleet host&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;e.g. Automation&lt;/font&gt;" vertex="1"> <mxGeometry height="65" width="190" x="60" y="500" as="geometry" /> </mxCell> <mxCell id="p3" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;" value="Fleet host&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;9 more, same pattern&lt;/font&gt;" vertex="1"> <mxGeometry height="65" width="190" x="830" y="30" as="geometry" /> </mxCell> <mxCell id="laptop" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;" value="Laptop&lt;br&gt;&lt;font style=&quot;font-size: 10px;&quot;&gt;roaming, off-LAN&lt;/font&gt;" vertex="1"> <mxGeometry height="65" width="190" x="830" y="500" as="geometry" /> </mxCell> <mxCell id="c1" edge="1" parent="1" source="cp" style="html=1;endArrow=none;dashed=1;strokeColor=#d6b656;strokeWidth=1.5;rounded=1;" target="p1"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="c2" edge="1" parent="1" source="cp" style="html=1;endArrow=none;dashed=1;strokeColor=#d6b656;strokeWidth=1.5;rounded=1;" target="p2"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="c3" edge="1" parent="1" source="cp" style="html=1;endArrow=none;dashed=1;strokeColor=#d6b656;strokeWidth=1.5;rounded=1;" target="p3"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="c4" edge="1" parent="1" source="cp" style="html=1;endArrow=none;dashed=1;strokeColor=#d6b656;strokeWidth=1.5;rounded=1;" target="laptop"> <mxGeometry relative="1" as="geoLine truncated
|
||||||
|
After Width: | Height: | Size: 235 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -169,7 +169,7 @@ Compliance hardening runs as a set of independently-tagged sub-roles, selected p
|
|||||||
>
|
>
|
||||||
> A third host breaks the model in a genuinely different way — not a variation on the same story, a different one entirely. Proxmox Backup Server runs as the vendor's own official Debian-based appliance image, deployed by hand and never a Terraform resource at all — there's no commented-out entry to preserve, because there was never a Terraform-managed version of it to convert away from. It still ended up on the tag-driven Ansible side of the pipeline: wired into its own inventory group and given a hand-written tag list after the fact, since compliance only ever checks whether a tag is present, not how a host arrived in the inventory. It's also the fleet's only non-Rocky host, which surfaced a real gap in one of the 9 hardening sub-roles above: `password_policy` configures PAM lockout behavior, and the ordering it assumes is Rocky's own — safe on every other host, but wrong on this one until it was fixed at the role level and explicitly re-validated.
|
> A third host breaks the model in a genuinely different way — not a variation on the same story, a different one entirely. Proxmox Backup Server runs as the vendor's own official Debian-based appliance image, deployed by hand and never a Terraform resource at all — there's no commented-out entry to preserve, because there was never a Terraform-managed version of it to convert away from. It still ended up on the tag-driven Ansible side of the pipeline: wired into its own inventory group and given a hand-written tag list after the fact, since compliance only ever checks whether a tag is present, not how a host arrived in the inventory. It's also the fleet's only non-Rocky host, which surfaced a real gap in one of the 9 hardening sub-roles above: `password_policy` configures PAM lockout behavior, and the ordering it assumes is Rocky's own — safe on every other host, but wrong on this one until it was fixed at the role level and explicitly re-validated.
|
||||||
>
|
>
|
||||||
> None of these three exceptions were retroactively fixed once the LXC-provider gap closed — the point of preserving history is that it stays history, not that it gets quietly cleaned up the moment it becomes possible to. But the gap itself is real and worth naming honestly too: for any *new* LXC going forward, this entire honest-exception story no longer applies. The fleet's [zero-trust mesh overlay](netbird-zero-trust.md) is the first host actually built through the LXC-native path above, start to finish, with no manual step anywhere in its provisioning.
|
> None of these three exceptions were retroactively fixed once the LXC-provider gap closed — the point of preserving history is that it stays history, not that it gets quietly cleaned up the moment it becomes possible to. But the gap itself is real and worth naming honestly too: for any *new* LXC going forward, this entire honest-exception story no longer applies. The mesh overlay in [`docs/netbird-zero-trust.md`](netbird-zero-trust.md) is the first host actually built through the LXC-native path above, start to finish, with no manual step anywhere in its provisioning.
|
||||||
|
|
||||||
---
|
---
|
||||||
Related: [`docs/secrets-and-pki.md`](secrets-and-pki.md), [`docs/security-monitoring.md`](security-monitoring.md), [`docs/disaster-recovery.md`](disaster-recovery.md), [`docs/netbird-zero-trust.md`](netbird-zero-trust.md)
|
Related: [`docs/secrets-and-pki.md`](secrets-and-pki.md), [`docs/security-monitoring.md`](security-monitoring.md), [`docs/disaster-recovery.md`](disaster-recovery.md), [`docs/netbird-zero-trust.md`](netbird-zero-trust.md)
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# 🕸️ Zero-Trust Mesh Overlay
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> A self-hosted **NetBird** WireGuard mesh gives every fleet host — and every device that needs to reach it — its own stable overlay identity, independent of where that device physically sits. Access is granted per-identity through mesh policy, not inferred from LAN position. The mesh is **deny-by-default**: nothing crosses it unless an explicit policy allows it, the same instinct behind every other boundary in [`docs/architecture-overview.md`](architecture-overview.md), applied to remote and roaming access specifically.
|
||||||
|
|
||||||
|
## Why a mesh, not a classic VPN
|
||||||
|
|
||||||
|
A traditional VPN concentrator was the obvious alternative, and it was rejected for the same reason a flat LAN got rejected everywhere else in this fleet: it's one more device sitting in a path, and reaching it means punching a hole to a single choke point rather than establishing an identity-scoped connection to exactly the host that's actually needed. A mesh instead gives every peer — fleet host or personal device — its own stable identity (`<host>.netbird.selfhosted`, a `100.91.0.0/16` overlay address) and lets WireGuard tunnels form directly, peer-to-peer, falling back to a relay only when direct connectivity isn't possible. Policy is written once, per identity or group, and travels with the peer regardless of which network it's physically on.
|
||||||
|
|
||||||
|
This is the actual shape of it — the control plane coordinates, but it's deliberately not where the traffic goes:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
That's the actual difference from a VPN concentrator, drawn rather than just asserted: a concentrator puts itself in every data path by design; here the control plane's spokes only ever carry coordination traffic, and every thick line is two peers talking directly to each other.
|
||||||
|
|
||||||
|
## Built on a capability that didn't exist yet
|
||||||
|
|
||||||
|
The mesh's own control plane is the first host in this fleet ever provisioned through a genuinely new capability: Terraform gaining native LXC support (a second provider, `bpg/proxmox`, alongside the existing VM-only one — full story in [`docs/iac-pipeline.md`](iac-pipeline.md)). Every other LXC in this fleet either predates Terraform entirely or was hand-converted after the fact; this one went through the pipeline start to finish with no manual provisioning step anywhere. That's not a coincidence — a lightweight mesh control-plane container was exactly the kind of workload that made closing the LXC gap worth doing.
|
||||||
|
|
||||||
|
## Control plane
|
||||||
|
|
||||||
|
One `netbirdio/netbird-server` container runs Management, Signal, the embedded relay, and NetBird's own embedded IdP together — combined-server mode, not four separate services. A digest-pinned `netbirdio/dashboard` container serves the admin SPA. This is the real container list, not a claimed one:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||||
|
NAMES IMAGE STATUS
|
||||||
|
netbird-proxy caddy:2-alpine Up 38 hours
|
||||||
|
netbird-server netbirdio/netbird-server:0.77.1 Up 38 hours
|
||||||
|
netbird-dashboard f3eb26c93ca9 Up 38 hours
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration is Vault-Agent-rendered, never committed — secrets live in Vault under a dedicated AppRole and policy scoped to this one host, the same pattern as every other secret in [`docs/secrets-and-pki.md`](secrets-and-pki.md). The identity provider is NetBird's own embedded one, not Authentik — Authentik is deliberately LAN-only in this fleet and publishing it wasn't in scope, so there's no OIDC migration path here; the first-login owner account is the permanent identity for this system specifically.
|
||||||
|
|
||||||
|
## The edge routing problem, and what actually broke
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **Learned the hard way during bring-up, not designed in from the start.** NetBird's Management and Signal services need native gRPC over HTTP/2-cleartext (h2c) end-to-end — any downgrade to HTTP/1.1 anywhere in the chain kills the bidirectional stream with an `unexpected EOF`, and the client agent just wedges with no useful error. The edge firewall's own reverse proxy can carry h2c to a single plain-HTTP backend, but it can't perform NetBird's own content-type/path split between the management API, the relay, and the dashboard SPA — so a small in-stack proxy does exactly that split, one hop further in. A second, unrelated thing broke the same week: the public DNS record for this service has to stay **DNS-only**, not proxied through a CDN — a proxying CDN returns an outright 403 to the gRPC-speaking agent, no explanation given, and the fix was disabling that proxying entirely for this one record rather than trying to work around it.
|
||||||
|
|
||||||
|
The real path a request takes:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And this is the actual splitter doing that work — the real config, not a description of the logic:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cat Caddyfile
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
auto_https off
|
||||||
|
servers {
|
||||||
|
protocols h1 h2 h2c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:33000 {
|
||||||
|
# native gRPC + gRPC-web (any application/grpc* content-type) -> server over h2c
|
||||||
|
@grpc header Content-Type application/grpc*
|
||||||
|
handle @grpc {
|
||||||
|
reverse_proxy h2c://netbird-server:80
|
||||||
|
}
|
||||||
|
|
||||||
|
# REST, OAuth, relay/signal WebSockets, and the gRPC service paths -> server
|
||||||
|
@srv path /api/* /oauth2/* /relay* /ws-proxy/* /signalexchange* /management*
|
||||||
|
handle @srv {
|
||||||
|
reverse_proxy netbird-server:80
|
||||||
|
}
|
||||||
|
|
||||||
|
# everything else -> dashboard SPA
|
||||||
|
handle {
|
||||||
|
reverse_proxy netbird-dashboard:80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fleet enrolment
|
||||||
|
|
||||||
|
A dedicated Ansible role installs the mesh agent and enrols a host, opt-in per host via the same tag-driven mechanism as every other role in [`docs/iac-pipeline.md`](iac-pipeline.md) — nothing joins the mesh by default just because it exists. Enrolment uses a reusable, class-scoped setup key fetched live from the secrets platform at play time, never stored in the repo, and read only on a host's first join. Because peer approval is required, a brand-new peer pauses for an operator to approve it in the dashboard before it gets any mesh access at all — there's no silent auto-join path.
|
||||||
|
|
||||||
|
All fleet hosts are enrolled today, plus two personal devices (a laptop with full mesh access, a home device with none by default — being *on* the mesh and being *allowed* anywhere on it are two separate, independently-granted things). Fleet servers run with local DNS resolution disabled on the mesh interface deliberately — they're not roaming, and the fleet's own internal DNS already handles them; only the one roaming device gets split-DNS behavior, resolving the fleet's internal zones over the mesh specifically because it's the one peer that actually needs to.
|
||||||
|
|
||||||
|
This is the real, live peer roster, not a claimed enrolment count — every fleet host plus both personal devices, each with its own overlay identity:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Zero-trust policy: deny-by-default, not just present
|
||||||
|
|
||||||
|
The mesh ships with a built-in allow-everything default policy — the first real security decision here was turning it off. With it disabled, no traffic crosses the mesh interface at all unless a specific policy grants it; LAN traffic is completely untouched by this, since the policy only governs the overlay interface, not the physical network.
|
||||||
|
|
||||||
|
**Groups** carry the actual access model:
|
||||||
|
|
||||||
|
| Group | Role |
|
||||||
|
|---|---|
|
||||||
|
| Admin | Full fleet access — the policy source for everything below |
|
||||||
|
| Servers | Every fleet host — coarse policy destination |
|
||||||
|
| Endpoints | Personal devices with no fleet access by default — on the mesh for connectivity, not for reach |
|
||||||
|
| Functional sub-groups | Hypervisors / infra / Docker hosts / storage — each server is in exactly one, no policy on them yet, there so future rules can be scoped precisely instead of broadly |
|
||||||
|
|
||||||
|
**Policies**, all sourced from the admin group and one-directional: SSH and hypervisor-management ports to servers (an explicit port list — the platform's own API rejects ranges, so nothing here is a lazy wildcard), ICMP for diagnostics, and DNS specifically to the infra sub-group to support split-DNS for the roaming device. A posture check gates admin-level access behind a minimum agent version and kernel version — an out-of-date or unpatched peer simply doesn't qualify for the policy regardless of which group it's in. Login expiration applies only to the human-authenticated peer; the fleet's own service-key peers don't expire on a timer, since there's no human session behind them to expire.
|
||||||
|
|
||||||
|
This is the real policy list, not a description of the intent — the built-in allow-all sitting disabled at the top, every real rule beneath it scoped to a specific source, direction, and an explicit port list rather than "allow the group and hope":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is the mesh actually enforcing that model, not a claimed state — a real enrolled peer's own live status:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ netbird status --detail
|
||||||
|
Peers detail:
|
||||||
|
[laptop]:
|
||||||
|
NetBird IP: [overlay address]
|
||||||
|
Status: Idle
|
||||||
|
-- detail --
|
||||||
|
Last WireGuard handshake: -
|
||||||
|
...
|
||||||
|
Events:
|
||||||
|
[INFO] SYSTEM
|
||||||
|
Message: Network map updated
|
||||||
|
Time: 5 minutes, 7 seconds ago
|
||||||
|
```
|
||||||
|
|
||||||
|
The network map updating on its own, unprompted, on a peer that isn't even the control plane, is the mesh's policy engine actually pushing live state to every connected peer — not a dashboard claiming a policy exists.
|
||||||
|
|
||||||
|
## Rollout, phased and honestly incomplete
|
||||||
|
|
||||||
|
Not everything is finished, and that's stated plainly rather than rounded up:
|
||||||
|
|
||||||
|
| Phase | Scope | State |
|
||||||
|
|---|---|---|
|
||||||
|
| Control plane + dashboard, embedded identity | Stand up the mesh itself | Done |
|
||||||
|
| First peers | Laptop + one fleet host, verified on-LAN | Done |
|
||||||
|
| Fleet enrolment | Every fleet host via the dedicated role | Done |
|
||||||
|
| Roaming | Split-DNS (done) + full-LAN route + off-LAN verification | Partly done |
|
||||||
|
| Zero-trust policy | Groups, keys, deny-by-default, approval, posture, login expiry | Done |
|
||||||
|
| Enforcement | Per-host firewall rejects non-mesh admin access | Planned |
|
||||||
|
|
||||||
|
The honest gaps: off-LAN connectivity (STUN/relay through NAT) hasn't been verified end-to-end yet, the full internal-network route for agent-less devices is still open, and per-host firewall enforcement — actually *requiring* the mesh for admin access rather than just offering it as one more path — is a deliberate later phase, not an oversight. A phased rollout that's honest about which phases aren't done yet is worth more than a status page that rounds every in-progress item up to "complete."
|
||||||
|
|
||||||
|
---
|
||||||
|
Related: [`docs/iac-pipeline.md`](iac-pipeline.md), [`docs/architecture-overview.md`](architecture-overview.md), [`docs/firewall-and-network.md`](firewall-and-network.md), [`docs/secrets-and-pki.md`](secrets-and-pki.md), [`docs/dns-naming.md`](dns-naming.md)
|
||||||
Reference in new issue
Block a user