Personal WireGuard VPN on AWS EC2. One command up, one command down, QR code for your phone. Infrastructure is a CDK stack per tunnel, so teardown is a clean CloudFormation delete.
(Demo is time-compressed; the QR shown is from throwaway keys.)
Plenty of tools get you a VPN; snorkel is for when you want one that is yours and disposable:
- vs commercial VPNs / Tailscale exit nodes — no third party in your traffic path and no subscription. It's your AWS account, a stock EC2 instance, and vanilla WireGuard.
- vs Algo / wireguard-install scripts — those configure a server you
already have; snorkel owns the whole lifecycle.
upcreates everything (VPC, instance, IP),downprovably deletes it all via CloudFormation, and devices are added or revoked live without touching the server. - Disposable by design — spin up an endpoint in any region for an evening, scan the QR, tear it down after. Nothing to maintain in between.
- Node.js >= 18
- AWS credentials configured (
aws configure, SSO, or--profile) - Admin-level AWS permissions:
snorkel upruns CDK under the hood (cdk bootstrap+cdk deploy), which creates IAM roles, EC2, VPC and SSM resources — and CDK's default bootstrap uses an AdministratorAccess execution policy. Read what gets created before pointing this at an account you care about. - The WireGuard app on your phone
npm install
npm link # makes the `snorkel` command available globally (optional)Without npm link, use npm run cli -- instead of snorkel.
# Spin up a tunnel in the region you want (~5 min: bootstrap + deploy + instance setup)
snorkel up --region eu-west-1
# Show the QR / plain config again later
snorkel qr
snorkel config > ~/laptop-wg.conf # for a laptop instead of a phone
# Add more devices — applied to the server live, no redeploy, no downtime
snorkel peer add laptop
snorkel peer add tablet
snorkel peer list
snorkel peer remove tablet # revokes that device's access immediately
# With several devices, pick one for qr/config
snorkel qr --peer laptop
snorkel config --peer laptop > ~/laptop-wg.conf
# See what's running
snorkel status
# Tear everything down (instance, VPC, EIP, keys)
snorkel downup creates the first device automatically (named phone). Every device gets
its own keys and VPN IP, so they can all be connected at the same time.
Multiple tunnels in different regions can coexist:
snorkel up --region ap-northeast-1 --name tokyo
snorkel qr --name tokyo
snorkel down --name tokyoOther options: --port <udp-port> (default 51820), --instance-type
(default t4g.small), --profile <aws-profile>.
- Install the official WireGuard app (iOS / Android).
- Run
snorkel up(orsnorkel qrif the tunnel already exists) — a QR code prints in the terminal. - In the app: + → Create from QR code → scan → name it → toggle on.
- Verify on a "what's my IP" site: it should show the tunnel's Elastic IP and the AWS region.
The QR code contains your private key — don't screenshot or share it. All
phone traffic (IPv4 + IPv6) routes through the tunnel while it's on. For a
laptop, import snorkel config output into the WireGuard desktop app instead.
Per tunnel, a CloudFormation stack (Snorkel-<name>) containing:
- a minimal single-AZ VPC (one public subnet, no NAT gateways)
- a
t4g.smallAmazon Linux 2023 instance with an encrypted root volume - an Elastic IP, so the endpoint survives instance restarts
- a security group exposing only the WireGuard UDP port
- an instance role limited to SSM core + the two tunnel-specific SSM parameters
First up in a region also runs cdk bootstrap (idempotent).
How peers reach the server: the CLI keeps the full peer set (public keys,
preshared keys, VPN IPs) in one SecureString SSM parameter. The instance has a
small snorkel-sync script that pulls it, rebuilds wg0.conf, and hot-applies
it with wg syncconf — existing connections stay up. peer add/peer remove
update the parameter and trigger the script via SSM Run Command; a systemd unit
also re-runs it at every boot, so changes made while the instance was down
still apply.
- No SSH. Port 22 is closed; shell access is via SSM Session Manager:
aws ssm start-session --target <instance-id> --region <region> - Server private key never leaves the instance — it is generated on first boot; only the public key is published (via SSM Parameter Store).
- Client private key never leaves your machine — it lives in
~/.snorkel/<name>/(mode 0600), embedded only in the config/QR you scan. - Preshared key adds a symmetric layer on top of the WireGuard handshake; it is delivered to the instance via an SSM SecureString parameter, never via user data (user data is readable by anyone with EC2 describe access).
- IMDSv2 required, encrypted gp3 root volume.
- Automatic security updates:
dnf-automaticapplies security updates daily and reboots when a kernel update requires it (the tunnel comes back by itself; expect a brief blip on those rare occasions).
Running a tunnel is not free. What you pay for:
- The EC2 instance. The default
t4g.smallis currently covered by AWS's Graviton free trial (750 hours/month for all accounts — enough for one tunnel running 24/7; a second simultaneous tunnel is billed). After the trial, or for other instance types, see EC2 pricing;--instance-type t4g.nanois the cheapest option and plenty for WireGuard. - The public IPv4 address, billed hourly (pricing).
- Data transfer out beyond AWS's monthly free allowance, plus a small amount of EBS storage (pricing).
Check the AWS Free Tier page for what your
account qualifies for, and remember snorkel down stops all tunnel charges.
- Local state (keys, endpoint, config) lives in
~/.snorkel/<name>/.snorkel downdeletes it; pass--keep-stateto retain the keys. - Every device has its own keys, so a lost device is revoked with
snorkel peer remove <device>— the others are unaffected. - Full key rotation / suspected server compromise:
snorkel down && snorkel upgives a fresh instance and brand-new keys (the endpoint IP changes; re-import on every device). snorkel downremoves everything the tunnel created but keeps the sharedCDKToolkitbootstrap stack in the region (S3 bucket, ECR repo, IAM roles — effectively free, and reused by the next deploy). Delete it in the CloudFormation console if you want the account completely clean.- If
uptimes out waiting for the server key, the instance is still booting or user data failed; check/var/log/snorkel-setup.logvia SSM. - Re-running
upwith a different--instance-typeor--portapplies the change by replacing the instance (the server key rotates; re-import configs on every device).
- No IPv6 on the tunnel. Client configs route
::/0into the tunnel, but the server has no IPv6 connectivity — v6 traffic is deliberately dropped rather than leaked around the VPN. Native IPv6 support would lift this. snorkel statusreports CloudFormation state from the regions in local state; it does not check live WireGuard health (a futurestatus --deepcould show per-device handshakes via SSM) and does not discover tunnels created from other machines.- Local state in
~/.snorkelis the source of truth for keys. Losing it means tearing the stack down via CloudFormation and starting over.
