Network: public, private VPC, AWS Client VPN¶
Laptops reach a deployment on two ports: 443 for the control plane and
5432 for Postgres, where every push goes. Both present a publicly
trusted certificate that laptops verify against their system trust store
(sslmode=verify-full), so there is no private certificate authority to
install and a renewal touches no laptop. There are two modes, and what
changes between them is who can reach those ports and how the certificate
is obtained:
- Public: the internet reaches the host, and the edge takes its certificate over port 80.
- Private VPC: nothing from the internet reaches the host. The certificate comes through a DNS record in a public zone, and laptops reach the host over a private network.
Where that private network is an AWS Client VPN, AWS Client VPN says what the deployment needs from it. Laptops that already reach the VPC some other way skip that section; the product needs no VPN of its own.
A deployment behind a CDN is a public one with four more steps (Behind a CDN). This page covers the network around the server; Install the server is the server itself.
Before you install¶
The install is self-serve, so settle these first. Each one decides a value
in .env, a record in DNS or a rule in the network.
| Settle this | Why | Where it goes |
|---|---|---|
| The public name or names: the control plane, and Postgres if it differs | DNS names, no port, never an IP address: the edge asks for certificates for names only, so a laptop verifying with verify-full could not check an address. In the dns mode they must also be lower-case |
PUBLIC_HOST, PUBLIC_PG_HOST (Fill in .env) |
| Whether the internet can reach the host on 80, 443 and 5432 | Decides the mode | CERTIFICATE_MODE: http if it can, dns if not |
| The DNS service that hosts the public zone, its zone id, and the region the helper's AWS client uses (a Route 53 zone has none) | The dns mode writes its challenge record through Route 53 only today |
AWS_HOSTED_ZONE_ID, AWS_REGION |
| Any CAA record on the zone or a parent | It must allow the authority: letsencrypt.org, and in the http mode ZeroSSL too, the authority the edge falls back to (check ZeroSSL's own CAA value) |
Your DNS service |
| The VPN product and how it attaches (subnet association, Transit Gateway, other) | Decides whether laptops share addresses, and so the jails | VPN_RANGES, and the same ranges typed by hand into the jails (The Postgres jail) |
| Whether the VPN signs each person in, or admits any laptop holding a shared certificate | A per-person sign-in names who is behind 5432; a shared certificate proves only that a laptop holds it | Your VPN |
| Split or full tunnel, and whether the VPN pushes the VPC's resolver | A laptop must resolve the private name over the tunnel and reach the identity provider and the hosts the laptop reaches: over its own internet with split tunnel, or through the VPC's internet egress (a NAT gateway, say) with full tunnel | Your VPN, AWS Client VPN |
| The address ranges laptops appear from, and the VPC and client ranges | None may overlap 172.16.86.0/24, the compose subnet, where pg_hba.conf admits every role, not only device roles, with a password. Ranges in VPN_RANGES must be IPv4, /30 or wider, and private: scripts/check-env.sh checks that, and nothing else on the server reads them |
VPN_RANGES; your VPC and VPN |
The instance metadata settings (dns mode on AWS, or Bedrock with the instance role) |
IMDSv2 required, hop limit 1, or 2 when Bedrock signs with the instance role (Amazon Bedrock); at hop limit 2 every container can use the role, so it should hold only what each use needs | The instance |
Outbound DNS (dns mode) |
The helper needs none: no outbound port 53 and no public resolver; the VPC resolver suffices. The check you run before the first start, scripts/check-dns-zone.sh, does: outbound UDP port 53 to the zone's own name servers |
The security group and network ACLs |
| Outbound https | Each mode has an allow-list | What the server reaches |
Choose a mode¶
| Mode | CERTIFICATE_MODE |
Reaches 443 and 5432 | Port 80 | VPN ranges (VPN_RANGES and the jails) |
|---|---|---|---|---|
| Public | http (or unset) |
the internet | the internet: the certificate challenge and the https redirect | none |
| Private, laptops share the VPN's addresses (a Client VPN associated with subnets) | dns |
the VPN only | optional, the redirect only | the associated subnets |
| Private, laptops keep their own addresses (a Transit Gateway attachment, or a network with no translation) | dns |
the private network only | optional, the redirect only | none |
Both private modes install from the bundle, as the public one does (Install the server).
On a server installed from the bundle, VPN_RANGES in .env is read only
by scripts/check-env.sh, which checks its shape. fail2ban never reads
.env: you type the same ranges by hand into the jail's ignoreip and the
VPN jail (The Postgres jail).
Public deployment¶
- Point the names at the server with an A record each. The server publishes its ports on IPv4 only, so give no AAAA record that leads to it.
- Open 80, 443 and 5432 to the internet, and nothing else. Port 80 carries the certificate challenge and the redirect to https. Nothing on the server checks which ports are public, so the firewall in front of it is yours to keep (Check the requirements).
- Set
.envwithCERTIFICATE_MODEunset orhttp(Fill in.env). - Check CAA: a record that names neither Let's Encrypt nor ZeroSSL, the authority the edge falls back to, refuses the certificate. Check ZeroSSL's own CAA value before you add it.
- Postgres stays locked down, whatever the network allows:
- from outside, only device roles (
+fleetview_devices), only over TLS, only with a SCRAM password (deploy/db/pg_hba.conf); the superuser and every owner role are refused before a password is read; - laptops connect with
sslmode=verify-fulland pin nothing; - the jail
fleetview-postgres, which you set up on the server, bans an address after five refused logins in ten minutes, for an hour, acting inDOCKER-USER(The Postgres jail); VPN_RANGESstays blank, and the jail ignores no VPN range.- Verify with Check the installation.
Behind a CDN¶
A deployment may put a CDN or reverse proxy in front of the edge. Every request then arrives from that network, so the peer address is the CDN's and the visitor's survives only in a header.
- Give Postgres its own name. A proxied name carries no port 5432, so
set
PUBLIC_PG_HOSTto a name that points at the server itself, not through the CDN. The edge certifies whichever names it serves. - Set
EDGE_TRUSTED_PROXIESin.envto the CDN's ranges, space-separated. The edge then believes the visitor's address from those networks and ignores it from everywhere else, so no client can claim an address by sending the header. Leave it unset when nothing sits in front. - The proxy must set
Cf-Connecting-Ipitself, overwriting any value the client sent, to the address it saw the visitor connect from; Cloudflare does. That is the only header the edge reads an address from:X-Forwarded-Foris a list a client can prepend to, and a proxy that passes a client's ownCf-Connecting-Ipthrough lets that client choose the address the edge believes. A proxy that sends onlyX-Forwarded-Forleaves the edge reading the proxy's own address, so the setting looks applied and changes nothing. - Leave the certificate challenge alone. In the
httpmode the challenge reaches the edge through the CDN, so the CDN must not answer or redirect/.well-known/acme-challenge/itself.
The edge's access log, docker compose logs edge in the install folder,
shows one line per request with client_ip (the visitor) beside
remote_ip (the peer). Two different values mean the trust works; two
identical ones mean the request did not come through a trusted proxy.
Private VPC: the certificate through DNS¶
This mode has run on AWS: the shipped dns-certs helper issued a
production certificate through a Route 53 public zone with the instance
role, on a clean install in a private network. A certificate for two names
has run only against a test authority (Known limits).
- One zone, public and private. Both names,
PUBLIC_HOSTandPUBLIC_PG_HOSTwhen it differs, live in one public Route 53 zone, because the helper is given one zone id. That zone holds nothing for them but the challenge records, which the helper writes and removes itself. A private hosted zone of the same name, associated with the VPC, holds an A record for each name to the server's private address; the VPC needs DNS support and DNS hostnames on. Inside the VPC each name resolves to the private address; outside it does not resolve, yet it can still be certified. -
Give the server its instance role with this policy and no more, the zone id and every challenge name filled in (one name when
PUBLIC_PG_HOSTisPUBLIC_HOST):{ "Version": "2012-10-17", "Statement": [ { "Sid": "ReadChangeStatus", "Effect": "Allow", "Action": "route53:GetChange", "Resource": "arn:aws:route53:::change/*" }, { "Sid": "ReadTheZone", "Effect": "Allow", "Action": "route53:ListResourceRecordSets", "Resource": "arn:aws:route53:::hostedzone/<ZONE_ID>" }, { "Sid": "WriteOnlyTheChallengeRecord", "Effect": "Allow", "Action": "route53:ChangeResourceRecordSets", "Resource": "arn:aws:route53:::hostedzone/<ZONE_ID>", "Condition": { "ForAllValues:StringEquals": { "route53:ChangeResourceRecordSetsNormalizedRecordNames": ["_acme-challenge.<PUBLIC_HOST>", "_acme-challenge.<PUBLIC_PG_HOST>"], "route53:ChangeResourceRecordSetsRecordTypes": ["TXT"] } } } ] }Require IMDSv2 at hop limit 1, or 2 when Bedrock signs with the instance role. The helper runs on the host network, so hop limit 1 is enough for it; at hop limit 2 every container can use the role, its Route 53 statement included, so keep its policy to what both uses need, or sign Bedrock with an API key and stay at 1.
ListHostedZonesByNameis not needed, because the zone id is given. -
Allow outbound https to the certificate authority and the Route 53 API (through a NAT gateway or its equal). The helper needs no outbound port 53 and no public resolver; the VPC resolver suffices (why is in
docker-compose.prod.yml, atCERT_DNS_WAIT). The check in step 5 does: allow outbound UDP port 53 to the public zone's own name servers, and outbound https to Docker Hub, where it looks up the helper's image. - Set
.env:CERTIFICATE_MODE=dns,AWS_HOSTED_ZONE_ID(the public zone's id, never the private one's),AWS_REGION, andACME_EMAIL, which compose still requires;scripts/check-env.shnames any it refuses. - Before the first start, run
scripts/check-dns-zone.shin the bundle folder (Fill in.env). With the instance role, it writes a test challenge record, asks each of the zone's name servers for it directly over port 53, removes it, and checks that Let's Encrypt answers and that the helper's image resolves at Docker Hub. Where only https may leave, its read-back fails although the issue could work. An install from the bundle then starts the helper and waits for its first certificate (Install the server). - Tell people first, and expect a gap at first install. Both names become public with the first certificate, in the public certificate transparency logs, and nobody can enroll until it exists (Certificate renewals).
- Security group: admit 443 and 5432 only from where laptops come from, 80 too if you want the redirect, and nothing from the internet.
- Postgres: the same rules as the public mode, step 5. Laptops must
dial the name, never the address, because
verify-fullfails by address. Whether the jails need the VPN's ranges depends on shared addresses (AWS Client VPN). - Verify from a machine on the private network, which resolves and reaches the host as laptops do (Check the installation).
Certificate renewals¶
No laptop is involved in a renewal: laptops verify against their system trust store, so a new certificate from the same public authority needs nothing from them.
httpmode. The edge renews its own certificate about thirty days before it expires. Within five minutesdb-certsinstalls it for Postgres and reloads it;docker compose logs db-certsshowsinstalled the edge's certificate for <PUBLIC_PG_HOST> (notAfter=<date>)andPostgres reloaded and presents the installed certificate.dnsmode. Thedns-certshelper checks twice a day and renews when renewal is due. The edge takes the new certificate within a minute, with no restart (docker compose logs edge-reloadshowsthe edge reloaded and presents the helper's certificate), and Postgres within five minutes. Until the first certificate exists the edge serves nothing and Postgres presents a short-lived self-signed one, so nobody can enroll. A helper that cannot issue retries an hour later;docker compose logs dns-certscarries lego's error.
Let's Encrypt no longer e-mails before a certificate expires, and nothing on a server installed from the bundle watches expiry, so check it yourself, on a schedule, from a machine outside the server:
echo | openssl s_client -connect <PUBLIC_PG_HOST>:443 -servername <PUBLIC_PG_HOST> 2>/dev/null | openssl x509 -noout -enddate
echo | openssl s_client -connect <PUBLIC_PG_HOST>:5432 -starttls postgres 2>/dev/null | openssl x509 -noout -enddate
Check both: the first is the certificate the edge holds for the name
laptops dial for Postgres, the one db-certs copies, and Postgres takes a
renewed one a few minutes after the edge. A 5432 date that falls behind
443's means db-certs did not take it (docker compose logs db-certs says
why). Ask 443 for PUBLIC_PG_HOST, not PUBLIC_HOST: where the two
differ, the http mode keeps one certificate per name, and behind a CDN
PUBLIC_HOST shows the CDN's certificate.
To force a renewal in the dns mode, run
docker compose exec dns-certs touch /data/.renew-force in the install
folder; the helper logs dns-certs: forced renewal done. Let's Encrypt
allows five duplicate certificates a week, so do it to test, not as a habit.
Recreate the stack with a full docker compose up -d, never the edge on its
own (up -d edge): in the dns mode that leaves the reload helper behind,
and no renewal reaches the edge until the next full up -d.
AWS Client VPN¶
Written from a real endpoint with three laptops and from AWS's documentation. Skip it if laptops reach the VPC another way. The VPN itself is yours to run; this is what the deployment needs from it.
The shape that was proved: an endpoint associated with one subnet, split tunnel, the VPC's resolver (the VPC's base address plus two) as its DNS server, one authorisation rule for the VPC's range, and mutual certificate authentication.
- The client range must not overlap the VPC, must be between /22 and
/12, and cannot be changed later. Keep it, and the VPC, clear of
172.16.86.0/24. - Split-tunnel routes must carry the VPC's range, or at least the server's subnet and the resolver. With full tunnel every route goes through the VPC, which then needs internet egress (a NAT gateway, say) to the identity provider and the hosts the laptop reaches. Routes and authorisation rules are per range, not per port, so the server's security group is what limits the ports. Changing the endpoint's route table resets every client.
- DNS works only where the client applies the pushed resolver. Proved:
the OpenVPN community client on Ubuntu with its
update-systemd-resolvedhook, which answered every query from the private zone. The same client without the hook never applied it: the name was not found, so nothing could push. Also proved, on 2026-11-03: the AWS-provided client 6.2.1 on macOS, over full tunnel, which applied the pushed resolver and answered the name from the private zone; whether the name then resolved through no other resolver was not checked. Check from a laptop on the VPN withgetent hosts <control-plane-host>on Linux ordscacheutil -q host -a name <control-plane-host>on a Mac; the answer must be the private address. - Association subnets of their own, holding nothing but the endpoint's interfaces: anything else in them gets the VPN's looser jail.
- The server's security group: with a subnet association, admit 443 and 5432 from the endpoint's security group (tested for 5432). A Transit Gateway endpoint cannot be referenced that way, so admit the client range instead (documentation only).
Laptops share addresses. With a subnet association Postgres sees each laptop as the address of one of the endpoint's interfaces in the association subnet: three laptops appeared as two addresses, and a laptop moved between them on reconnect. A laptop with a stale device password retries every 30 seconds, two refusals a minute, and never exits, so the strict jail would ban everyone behind its address within three minutes. Therefore:
- declare the whole association subnets, never single addresses: the VPN
recreates its interfaces and a laptop moves between them, so only the
subnet holds still.
scripts/check-env.shrefuses a single address inVPN_RANGES, and that check is allVPN_RANGESdoes; - type the same subnets by hand into
fleetview-postgres'signoreip, and into the second jailfleetview-postgres-vpn, as The Postgres jail shows, with how to size it and find the stale laptop.
With a Transit Gateway each laptop keeps its own address (documentation
only): leave VPN_RANGES blank, add no second jail, and the strict jail
applies per laptop.
Host hardening is yours: fail2ban, the firewall and the operating
system. The product ships no jail and no filter. The jail must act in
DOCKER-USER (a default action on INPUT never sees traffic to a
published container port), and its filter must be proved against the
database container's journal before it is trusted
(The Postgres jail).
The Postgres jail¶
Port 5432 answers everyone who can reach it, so a stranger can try
passwords. pg_hba.conf lets only device roles reach a password check, and
a fail2ban jail you set up on the server bans an address that keeps
failing. The product ships neither the jail nor its filter; both are
yours to write and keep.
- Name it
fleetview-postgres. Ban inDOCKER-USERafter five refused logins in ten minutes, for an hour, reading the database container's journal: the bundle's compose files log that container to journald, with the client address on every line. Its container isfleetview-db-1, or<project>-db-1when.envsets anotherCOMPOSE_PROJECT_NAME. - Put the stack's own subnet in
ignoreip,172.16.86.0/24, so the stack's own misconfiguration is never a ban, and no wider private range than the declared VPN ranges: a VPC or VPN client guessing passwords is outside. - Anchor the filter after the journal prefix. fail2ban's systemd
backend puts
<host> <identifier>[<pid>]:before each entry, so a filter anchored at column 0 matches nothing. Match only a line with the Postgres timestamp right after that prefix: the refusal echoes the user name the client sent, and a name with a newline in it becomes indented continuation lines that could spell out a fake address. - Prove the filter against the journal before you trust it, and again
after a Postgres upgrade, which can reword a refusal:
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/fleetview-postgres.confmust count every refusal as matched.
A ban lifts itself after an hour. A laptop with a stale device password bans
itself: its pushes then fail on a refused connection instead of a refused
password, and winner push status says they are failing. Its owner runs
winner login, which enrolls it again with a fresh password. On the
server:
sudo fail2ban-client status fleetview-postgres # banned addresses, counts
sudo fail2ban-client set fleetview-postgres unbanip <address> # let it back in now
journalctl CONTAINER_NAME=fleetview-db-1 --since '-10min' -o cat | grep 'FATAL' | cat -v # what was refused, and why
cat -v matters: the user name in a refusal is whatever the client sent.
Twenty refusals across addresses in ten minutes is a scan; fewer, from one
address, is a laptop looping on a stale password.
Laptops that share the VPN's addresses¶
Where a VPN hands every connected laptop the address of one of its
interfaces (AWS Client VPN), one stale laptop would ban
everyone behind its address. Declare those ranges in VPN_RANGES, where
scripts/check-env.sh checks their shape and nothing reads them further.
Then type the same ranges by hand into fleetview-postgres's ignoreip,
and add a second jail named exactly fleetview-postgres-vpn on the same
filter and action that counts only those ranges, with a higher count and a
short ban. Each stanza needs enabled = true, since fail2ban leaves a jail
off by default; its filter, backend and action are yours to write:
[fleetview-postgres]
enabled = true
ignoreip = 127.0.0.1/8 ::1 172.16.86.0/24 <the VPN ranges>
maxretry = 5
findtime = 10m
bantime = 1h
[fleetview-postgres-vpn]
enabled = true
# same filter, backend and action as fleetview-postgres; the address goes
# quoted as one argument, then the VPN ranges, and exit 0 (ignore) means outside
ignorecommand = python3 -c 'import ipaddress,sys; a=ipaddress.ip_address(sys.argv[1]); sys.exit(any(a in ipaddress.ip_network(n) for n in sys.argv[2:]))' '<ip>' 10.45.2.0/24 10.45.3.0/24
maxretry = 60
findtime = 10m
bantime = 10m
Sizing it. A stale laptop writes about two refusals a minute, so sixty in ten minutes tolerates two stale laptops behind one address and bans at three; raise the count for more. A stale laptop trips the jail again within minutes of each lift, so the ban is short on purpose. Anyone on the VPN, and any machine in its subnets, gets the higher count, which is why the association subnets should hold nothing else.
Finding the stale laptop. The address in a refusal is the VPN's; the
role name is the device. Every device role is dev_ and twelve hex digits.
Check the name's shape and hand it to psql as a bound variable, never pasted
into the SQL, in the install folder:
ROLE=dev_0123456789ab # the role name from the refusal
[[ $ROLE =~ ^dev_[0-9a-f]{12}$ ]] && docker compose exec -T -e ROLE="$ROLE" db \
sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v role="$ROLE"' \
<<<"SELECT u.email, d.machine_name, d.hostname, d.status FROM control.devices d JOIN control.users u ON u.id = d.user_id WHERE d.pg_role = :'role'"
Its owner runs winner login. A role with no row, or a revoked one, is a
laptop still pushing after its device was revoked; its owner signs in again
or stops the push (winner push stop). Then let the address back in:
sudo fail2ban-client set fleetview-postgres-vpn unbanip <address>.
A laptop off the VPN¶
In a private deployment a laptop off the VPN cannot reach the host: the name does not resolve (the public zone holds no record for it), or the private address does not answer.
- A push fails to connect and writes no Postgres log line, so no refusal is counted and nothing is banned.
- The push service keeps running and retries every 30 seconds.
winner push statuswarns once two pushes in a row have failed. - A network change in the middle of a push, such as joining or leaving
the VPN, leaves the old connection open on the server, holding locks the
laptop's next push waits on. In the 2026-11-03 rehearsal that stall lasted more than twelve
minutes, until the connection was ended by hand.
Since 0.13.1 the production Postgres's keepalive settings (
docker-compose.prod.yml) drop a dead connection in about two minutes, and the push carries on. Until then the push neither fails nor finishes, sowinner push statuswarns of nothing. - Home shows the laptop Stale after 24 hours without a push.
winner login,winner updateand the hourly report need the control plane, so they fail until the laptop is back; the scheduled update simply tries again at its next run.- Sessions wait on the laptop. They stay in the laptop's local session store, and the next push that lands sends them. That is how it is designed; a long time off the VPN followed by the catch-up has not been run (Known limits).
What the server reaches¶
Outbound from the server, by mode. No licence server is contacted: a licence file is checked locally, on the host.
| Host | Public | Private (dns) |
For |
|---|---|---|---|
| Let's Encrypt's ACME API | yes, and ZeroSSL's, the authority Caddy falls back to | yes (Let's Encrypt only) | the certificate (Certificate renewals) |
| Inbound port 80 from the authority | yes | no | the HTTP challenge |
| The Route 53 API | no | yes | the challenge record |
| The public zone's own name servers, on UDP port 53 | no | while scripts/check-dns-zone.sh runs; the helper itself needs none |
the check's read-back of its test record |
| Instance metadata (link-local) | when Bedrock uses the instance role | yes | the instance role |
| Your identity provider (on Auth0, your tenant) | yes | yes | sign-in and tokens, and on Auth0 the Management API |
| Docker Hub, and the storage hosts it redirects downloads to | yes | yes, and for scripts/check-dns-zone.sh's lookup of the helper's image |
the stock images, pulled by digest; ours come from images.tar (Install the server) |
ghcr.io, GitHub's registry |
when trial is in COMPOSE_PROFILES |
same | the trial's sign-in provider (Dex), pulled by digest |
The Ollama model registry (registry.ollama.ai), and the storage host it redirects downloads to (today *.r2.cloudflarestorage.com) |
when the compose Ollama pulls a model | same | embeddings and a local model |
raw.githubusercontent.com, openrouter.ai |
yes | yes | model prices for cost figures, refreshed when the control plane sets up the organisation's schema (What leaves the server) |
| A hosted model endpoint | when one is set | same | Model and telemetry |
Amazon Bedrock runtime, bedrock-runtime.<region>.amazonaws.com |
when the provider is bedrock |
same | Amazon Bedrock |
| Sentry, PostHog | when set | same | Telemetry |
The jobs sweep's dead-man's switch (FLEETVIEW_HEARTBEAT_URL), and wherever you copy backups |
when set | same | Jobs, Back up, restore and uninstall |
| Your distribution's package mirror | while preparing the host | same | Docker, jq, rclone, fail2ban |
Setting up an organisation's schema refreshes model prices from GitHub
and OpenRouter (the row above). Unconfirmed: whether the Dashboard
(pg serve) does too. Its usage ping is off; no switch for the price refresh
is known.
Laptops reach what What the laptop sends, and where lists. In a private deployment the control plane and Postgres go through the tunnel. With split tunnel the other hosts go over the laptop's own internet; with full tunnel they go through the VPC too, so its egress must reach them. Sign-in sends the browser to the identity provider; the laptop itself contacts it only on a control plane without its own sign-in, as that section says.
How the private path was proved¶
In four parts, none of which alone covers it.
- A test rig with no cloud account. In it the production
dns-certshelper against a local test authority obtained one certificate for both names and Postgres presented it; a forced renewal reached Postgres with no laptop touched; three laptops behind one gateway pushed withverify-full, and Postgres logged one address for all three; a laptop with a stale password wrote two refusals a minute while the others kept pushing. - A real AWS account, the certificate: lego issued from inside a private network through a real Route 53 public zone beside a same-name private zone, with the instance role and the one-record policy, against Let's Encrypt's staging authority.
- A real AWS account, the laptops: a real Client VPN
endpoint and three Ubuntu laptops; the pushed resolver answered from the
private zone,
verify-fullpassed against the unmodified system trust store with a production Let's Encrypt certificate, and the strict jail's shared-address ban and the second jail's fix were both shown. - A real AWS account, a customer-shaped install (2026-11-03): the bundle on a clean server in a private
network, the shipped
dns-certshelper issuing a production certificate through Route 53 with the instance role, and a Mac on a full-tunnel Client VPN with the AWS-provided client pushing every session over it.
Known limits¶
- Two VPN set-ups were proved on AWS, both a subnet association with mutual certificates: split tunnel with the OpenVPN community client on Ubuntu 24.04, and full tunnel with the AWS-provided client on macOS. Not proved: a Transit Gateway, SAML or federated sign-in, IPv6, and a second association in another zone.
- Your VPN set-up is yours: the product, its routes, authorisation, pushed DNS and client configuration.
- The
dnsmode is Route 53 only. On AWS, bare lego issued one name per run from staging, and a production certificate for one name. The shippeddns-certshelper issued a production certificate through Route 53 in the 2026-11-03 install. A two-name certificate ran only in the rig, against its test authority, and the rig has no Route 53, so the policy condition listing two challenge names has not run; scheduled renewal under Compose was not run on AWS. - The rig trusts its test authority's root, and has no jail, journald,
Client VPN,
winnerbinary, enrolment or published port. - The jail was proved only with a stand-in filter; the server's own is the operator's to write and prove.
- Real laptop networks: a Mac joining the VPN in the middle of a push was seen, and the stall it caused is bounded (above). Sleep and wake, captive portals and other Wi-Fi changes were not tested.
- A long time off the VPN followed by the catch-up was not run.
- A name served both publicly and privately at once, so a laptop pushes over either path, was not tested.