The Consent Dongle: Zero‑Trust Client Servers with Fedora CoreOS
- Scott Tansowny
- Linux distributions , Home server , Security
- September 9, 2026
Updated September 9, 2026
I deploy Linux servers for clients many of which are not technical and I don’t want them to worry about learning technical concepts like SSH keys, firewall rules, or package managers. I want to deploy for them a server that works, stays secure, and doesn’t require them to think about it.
But they also deserve control over who can access their machine and confidence that their machine and data are their own.
This post describes the system I’ve built to give them exactly that: Fedora CoreOS, provisioned automatically from a USB stick, connected to me via WireGuard, and protected by a physical consent dongle—the same USB they used to install the OS.
When they plug in the USB, I can get in to provide support, when they unplug it, I’m locked out. Just a physical “key” to control access and a zero-trust system.
The Goal: Remote Access Without Compromising Client Privacy
I wanted to set up a system where:
- SSH is not exposed to the internet: this is a constant attack target.
- Port forwarding is not necessary: potentially confusing for clients and can bring in security concerns with open ports.
- There is no permanent VPN access: I can only reach the client’s server when they give explicit permission.
- The process is simple: the client simply boots from a USB drive and then uses that drive as a physical consent dongle.
- Our connection is secure: I have an encrypted tunnel to the client for setup and support.
- The system is reproducible: a single configuration file allows me to set up the entire system.
Fedora CoreOS and Wireguard made all of this possible.
The Process Overview
Here’s the architecture:
- The client receives or creates a custom USB stick containing a customized Fedora CoreOS installer.
- They boot from it. The OS installs itself automatically to their hard drive.
- The same USB becomes the consent dongle. After installation, the USB has only one job: to act as a physical key. Plug it in, and the WireGuard tunnel comes up. Unplug it, and the tunnel drops.
- I connect through WireGuard from my laptop and SSH in to provide installation, configuration, and support.
- CoreOS updates itself atomically in the background. If an update breaks something, the client can reboot and roll back easily.
The client never touches a terminal. They never configure a firewall. They never run updates. They just plug in a USB stick when they need my help.
Part 1: The WireGuard Server (Alpine Linux + Docker)
I run a WireGuard server on an Alpine Linux box in my own server. It acts as the hub for all client connections.
Setup
Assuming Docker is already running on your Alpine server:
# Install userspace tools
doas apk add wireguard-tools
# Load the kernel module
doas modprobe wireguard
echo wireguard | doas tee -a /etc/modules-load.d/wireguard.conf
Create the Docker Compose stack:
mkdir -p ~/docker/wireguard && cd ~/docker/wireguard
nano docker-compose.yml
services:
wireguard:
image: linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=America/Edmonton #replace with your timezone
- SERVERURL=my.domain.com #replace with your domain
- SERVERPORT=51820
- PEERS=99
- PEERDNS=1.1.1.1 #replace with your choses dns server
- INTERNAL_SUBNET=10.99.0.0/24
- ALLOWEDIPS=0.0.0.0/0
volumes:
- ./config:/config
- /lib/modules:/lib/modules:ro
ports:
- 51820:51820/udp
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
networks:
- wg_net
networks:
wg_net:
driver: bridge
Start the container:
docker compose up -d
Restricting Client Access
I don’t want client servers to reach each other—or my own LAN. So I add iptables rules to isolate them.
Edit ~/docker/wireguard/config/wg_confs/wg0.conf and replace the PostUp/PostDown lines:
PostUp = iptables -I FORWARD 1 -i %i -m iprange --src-range 10.99.0.12-10.99.0.100 -d 10.99.0.1 -j ACCEPT ; iptables -I FORWARD 2 -i %i -m iprange --src-range 10.99.0.12-10.99.0.100 -d 10.99.0.0/24 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT ; iptables -I FORWARD 3 -i %i -m iprange --src-range 10.99.0.12-10.99.0.100 -j DROP ; iptables -A FORWARD -i %i -j ACCEPT ; iptables -A FORWARD -o %i -j ACCEPT ; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -m iprange --src-range 10.99.0.12-10.99.0.100 -j DROP ; iptables -D FORWARD -i %i -m iprange --src-range 10.99.0.12-10.99.0.100 -d 10.99.0.0/24 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT ; iptables -D FORWARD -i %i -m iprange --src-range 10.99.0.12-10.99.0.100 -d 10.99.0.1 -j ACCEPT ; iptables -D FORWARD -i %i -j ACCEPT ; iptables -D FORWARD -o %i -j ACCEPT ; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE
Restart the container:
docker compose restart wireguard
Peer Access Tiers
- peer1–peer10 (IPs
10.99.0.2–10.99.0.11): Admin peers. Full access to the network and other peers. - peer11–peer99 (IPs
10.99.0.12–10.99.0.100): Client servers. Can only reach the WireGuard server itself and accept connections initiated by admins.
This means even if a client server is compromised, it cannot reach my infrastructure or other clients.
Part 2: My Admin Laptop
My laptop is an admin peer with IP 10.99.0.3.
sudo dnf install wireguard-tools
I copy the generated peer config from the Alpine server:
scp user@<machine ip>:~/docker/wireguard/config/peer2/peer2.conf ~/
sudo mv ~/peer2.conf /etc/wireguard/wg0.conf
sudo chmod 600 /etc/wireguard/wg0.conf
Then connect:
sudo wg-quick up wg0
sudo wg show
And optionally enable auto‑start:
sudo systemctl enable wg-quick@wg0
Part 3: Building the Custom Fedora CoreOS ISO
This is the really cool part. I create a customized ISO that installs CoreOS automatically and configures everything before first boot.
Install Needed Tools
sudo dnf install butane coreos-installer mkpasswd
Generate WireGuard Keys for the Client
On my laptop:
wg genkey | tee client_private.key | wg pubkey > client_public.key
wg genpsk > client_preshared.key
Create the Butane Configuration
Here’s the template I use. Replace the placeholders with actual values.
variant: fcos
version: 1.7.0
passwd:
users:
- name: user1
ssh_authorized_keys:
- <ssh key>
password_hash: <password hash>
home_dir: /home/user1
groups:
- docker
- wheel
shell: /bin/bash
storage:
files:
- path: /etc/wireguard/wg0.conf
mode: 0600
contents:
inline: |
[Interface]
Address = 10.99.0.12
PrivateKey = CLIENT_PRIVATE_KEY
ListenPort = 51820
PostUp = iptables -I INPUT 1 -s 10.99.0.0/24 -j ACCEPT
PreDown = iptables -D INPUT -s 10.99.0.0/24 -j ACCEPT
[Peer]
PublicKey = SERVER_PUBLIC_KEY
PresharedKey = CLIENT_PRESHARED_KEY
AllowedIPs = 10.99.0.1/32, 10.99.0.3/32
Endpoint = MY.DOMAIN.COM:51820
PersistentKeepalive = 25
- path: /etc/udev/rules.d/99-wireguard-killswitch.rules
mode: 0644
contents:
inline: |
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd*[0-9]", ENV{ID_FS_LABEL}=="fedora-core*", RUN+="/usr/bin/systemctl start wg-quick@wg0"
ACTION=="remove", SUBSYSTEM=="block", KERNEL=="sd*[0-9]", ENV{ID_FS_LABEL}=="fedora-core*", RUN+="/usr/bin/systemctl stop wg-quick@wg0"
systemd:
units:
- name: wg-quick@wg0.service
enabled: false
Convert and Customize the ISO
butane --pretty --strict config.bu > config.ign
wget https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/44.20260802.3.1/x86_64/fedora-coreos-44.20260802.3.1-live-iso.x86_64.iso #replace with current latest iso
coreos-installer iso customize \
--dest-device /dev/sda \
--dest-ignition config.ign \
--dest-console ttyS0,115200n8 \
--dest-console tty0 \
-o clientinstall.iso fedora-coreos-44.20260802.3.1-live-iso.x86_64.iso #replace with downloaded iso
The resulting clientinstall.iso will wipe /dev/sda and install CoreOS automatically when booted. If client needs a different disk, adjust accordingly.
Warning: The customized ISO will automatically wipe the disk specified in the Butane file. Make sure the target machine doesn’t have any other important data on that disk.
Part 4: The Consent Dongle
The client receives or creates the USB stick with instructions:
“Plug this into your server and turn it on. Wait for it to finish. When you need my help, plug it in again and tell me. If it’s plugged in, I have access—if it’s unplugged I do not.”
That’s the entire user manual.
After Installation
Once CoreOS is installed and the machine reboots, the USB is no longer bootable—but it’s still the consent key.
- Plugged in: The udev rule starts WireGuard. I can SSH in from my laptop.
- Unplugged: WireGuard stops. Access is severed immediately.
The client holds physical control over my access. That’s the zero‑trust model.
Making the USB Unbootable (But Still a Consent Dongle)
I do this on first login to ensure the USB can never accidentally reinstall the OS:
lsblk -o NAME,LABEL,SIZE,MODEL
Identify the USB, then:
sudo fdisk /dev/sdX
# Create new DOS partition table, new primary partition, write
sudo mkfs.vfat -I -F 32 -n "fedora-core" /dev/sdX
The label fedora-core matches the udev rule, so the drive still works as the consent dongle—but it can no longer boot.
Part 5: Security Considerations
This system is built on a few key principles:
1. Zero Trust
The client doesn’t have to trust me to “do the right thing.” They hold the physical key. No USB plugged in, no access. Period.
2. Minimal Attack Surface
The server has:
- No open ports (WireGuard connects out)
- No public exposure of any kind
- A minimal, locked-down core OS that updates atomically with minimal packages and a container-first design
3. Segmented Access
Client servers can’t reach each other or my LAN. Even if one is compromised, the rest are isolated.
4. Reproducibility
The entire system is defined in a Butane file and a customized ISO. If the server dies or the USB is lost, I can recreate everything in minutes.
Part 6: What If Something Goes Wrong?
The USB consent dongle is lost
I walk the client through creating a new one. Any USB with a label starting fedora-core will work as a consent key once CoreOS is installed.
An update breaks something
Fedora CoreOS keeps the previous image by design. The client reboots and selects the previous image from the boot menu. No complicated recovery process.
The client wants to revoke my access permanently
They unplug the USB and throw it away. That’s it. I no longer have access of any kind.
Final Thoughts
This system gives my clients something important: security without complexity. They don’t need to understand SSH, firewalls, or VPNs. They need to understand one thing: “Plug in the USB when I need help.”
And when they unplug it, they know—with absolute certainty—that I’m locked out.
All together it creates a server that is simple, secure, and private.
Want a Hand With Your Setup?
This is exactly the kind of system I build for my clients. If you’d like a zero‑trust server like this—or help with any Linux project—take a look at my support plans.
Take a look at my support plans →


