Providers#
What Is a Provider?#
A provider is a backend service that handles a piece of foundational infrastructure your self-hosted solution needs to function. Think of providers as the plumbing behind the scenes — they manage DNS resolution, IP address assignment, and SSL certificates so your apps are reachable by name with secure HTTPS connections. The three networking providers (local DNS, WAN DNS, DHCP) are the core of how networking works in PSW.
Providers are not optional extras you bolt on later. They’re mandatory infrastructure that PSW depends on from bootstrap onwards. Without them, targets wouldn’t get IP addresses, apps wouldn’t have DNS (Domain Name System) names, and HTTPS (secure web traffic) wouldn’t work.
Why Do They Exist?#
Imagine you add Jellyfin
to your self-hosted solution. For it to be accessible at https://jellyfin.yourdomain.ca, several things need to happen behind the scenes:
- The target
running Jellyfin needs a stable IP address (planned in
network.yml, protected by a DHCP — Dynamic Host Configuration Protocol — squat reservation) - The domain
jellyfin.yourdomain.caneeds to resolve to the right IP on your local network (local DNS) - If you want to access it from outside your home, the domain needs a public DNS record too (WAN DNS)
- Traefik needs a valid SSL certificate so HTTPS works (ACME)
Providers handle all four of these automatically. You configure them once, and PSW uses them every time it creates a target or deploys an app.
The Three Provider Types#
Local DNS#
What it does: Makes your apps reachable by name inside your network, and resolves the rest of the internet privately.
When you type https://jellyfin.yourdomain.ca in your browser at home, your router’s DNS resolver needs to know that this domain points to your Traefik server’s internal IP (e.g., 10.10.0.100). The local DNS provider creates these internal DNS records automatically. It also handles every other DNS query your devices make (Google, GitHub, your bank) — and PSW always configures it for recursive resolution, meaning queries go straight from root → TLD → authoritative servers without passing through Cloudflare/Quad9/etc. No third party sees your DNS pattern.
This is called split-horizon DNS — the same domain name resolves to different addresses depending on where you are:
- At home:
jellyfin.yourdomain.ca→10.10.0.100(your internal Traefik IP) - On the internet:
jellyfin.yourdomain.ca→ your public IP or tunnel address
| Required? | Yes |
| Backends | opnsense_unbound
(preferred when you have OPNsense) · adguard_home
(when you don’t) |
| What it manages | Internal hostname overrides + recursive resolution for everything else |
PSW supports two backends for the same job — pick one at wizard time based on whether OPNsense is your router:
opnsense_unbound#
OPNsense ships with Unbound
, a full-featured recursive DNS resolver. When you have an OPNsense box doing the firewall/routing/DHCP work, PSW reuses that Unbound instance for split-horizon — no extra service to run. The provider talks to the OPNsense API (/api/unbound/*) to write host overrides, and asserts at every bootstrap/convergence cycle that Unbound is in recursive mode (general.forwarding = "0") — that’s the privacy property.
adguard_home#
When OPNsense isn’t in the picture (your ISP/home router is the gateway), PSW takes over the local-DNS job itself. AdGuard Home runs as a Podman container on the bootstrap target , with Unbound as a recursive sidecar bound to localhost — same privacy guarantee. AdGuard handles host overrides via its REST API, ad/tracker blocking, per-client policies, and a query-log dashboard. You point your router’s DHCP “DNS server” option at the bootstrap target’s IP and you’re done.
The provider is HA-ready: it accepts a list of AdGuard instances and fans every write across all of them. v1 ships with one instance (the bootstrap target); growing to N is a config-only change.
WAN DNS#
What it does: Makes your apps reachable by name from the internet, and provides credentials for automatic SSL certificates.
If you want to access your self-hosted solution from outside your network (via remote access ), the WAN DNS provider creates public DNS records pointing to your tunnel endpoint. It also provides the API credentials that Traefik uses to obtain Let’s Encrypt SSL certificates via DNS-01 challenge.
| Required? | No — only needed for external access and ACME certificates |
| Current backend | Cloudflare |
| What it manages | Public DNS records + ACME certificate credentials |
DHCP#
What it does: Keeps planned target IPs from colliding with the dynamic pool that hands addresses out to your phones/laptops/TVs.
When PSW creates a new managed target
on Proxmox
, the LXC boots with the static IP planned in network.yml — it never sends a DHCP request. The DHCP provider’s job is to make sure that IP stays reserved for the target. How it does that depends on whether PSW can program the upstream DHCP server.
| Required? | Yes |
| Backends | opnsense_kea
(when OPNsense runs your DHCP) · isp_router_partition
(when your ISP/home router does) |
| What it manages | Either real reservations or partition validation — same protection from a different angle |
opnsense_kea#
When OPNsense is your DHCP server, PSW uses the Kea
plugin’s REST API to create a squat reservation — a record marked Managed by psw that pins each target’s planned IP. The reservation never binds at lease time; it only exists so the OPNsense pool allocator can never hand the address to a non-PSW device.
isp_router_partition#
When your ISP/home router is the DHCP server, PSW can’t program it (no API). Instead, the wizard asks for the router’s dynamic-allocation pool range (e.g. 192.168.1.50–192.168.1.250) and you carve out a static-only zone outside it (e.g. plan all targets at 192.168.1.10–192.168.1.49). The provider’s job becomes validation: every bootstrap and convergence cycle checks that no planned target IP has crept into the ISP pool range, and fails loudly if it has. No router-side writes — the protection comes from the partition.
ACME (SSL Certificates)#
ACME (Automatic Certificate Management Environment) is the protocol that Traefik uses to automatically obtain and renew TLS/SSL certificates (the encryption that makes HTTPS work) from Let’s Encrypt . It’s tied to the WAN DNS provider because the most common method (DNS-01 challenge) requires creating a temporary DNS record to prove you own the domain.
When configured, Traefik uses the WAN DNS provider’s API credentials (e.g., Cloudflare API token) to:
- Create a temporary DNS record for the challenge
- Let’s Encrypt verifies the record exists
- Let’s Encrypt issues the certificate
- Traefik deletes the temporary record
This all happens automatically — you never have to think about certificates after initial setup.
ACME config lives inside the WAN DNS provider configuration:
providers:
wan_dns:
backend: cloudflare
acme:
email: admin@example.com
challenge: dnsHow to Configure Providers#
The fast path — the Setup Wizard’s Start step
. When you fill in the OPNsense URL + API key + API secret and the Cloudflare API token on the Start screen, PSW does the whole thing for you in one pass: it verifies OPNsense is reachable, encrypts both sets of credentials into secrets/providers.yml, and registers all three provider backends (local-dns: opnsense-unbound, dhcp: opnsense-kea, wan-dns: cloudflare) in project.yml. You don’t run any psw provider configure commands by hand.
The manual path — psw provider configure. Use this when you want to reconfigure a provider later, swap backends, or script an unattended setup outside the wizard:
# Local DNS — tell PSW to use OPNsense Unbound
psw -C ~/my-project provider configure local-dns opnsense-unbound \
--secret-file ~/Downloads/opnsense-apikey.txt
# WAN DNS — tell PSW to use Cloudflare
psw -C ~/my-project provider configure wan-dns cloudflare \
--secret api_token=your-cloudflare-token
# DHCP — tell PSW to use OPNsense Kea
psw -C ~/my-project provider configure dhcp opnsense-keaWhen backends share the same device (like OPNsense Unbound and Kea both connecting to your OPNsense router), they share the same credentials — you only need to provide the API key once.
Where Configuration Is Stored#
Provider configuration is split into two files:
project.yml — which backend to use (safe to commit):
providers:
local_dns:
backend: opnsense_unbound
wan_dns:
backend: cloudflare
zone_id: abc123
acme:
email: admin@example.com
challenge: dns
dhcp:
backend: opnsense_keasecrets/providers.yml — connection credentials (encrypted with SOPS
). In OPNsense mode:
opnsense:
url: https://10.10.0.1
api_key: <encrypted>
api_secret: <encrypted>
verify_ssl: false
cloudflare:
api_token: <encrypted>In ISP-router mode there is no OPNsense entry — instead, the AdGuard admin credentials are listed under adguard_home:
adguard_home:
instances:
- target: core # network.yml target name running AdGuard
api_user: admin
api_password: <encrypted>
web_port: 3000 # optional, default 3000
cloudflare:
api_token: <encrypted>This separation means your backend choices are version-controlled and visible, while your passwords and API keys stay encrypted.
When Are Providers Used?#
Providers are called automatically at key moments — you never interact with them directly after configuration.
During Bootstrap #
- DHCP provider reserves an IP for the bootstrap target
- Core apps are deployed
- Local DNS provider creates internal records for all core apps (e.g.,
dashboard.yourdomain.ca→ Traefik IP) - WAN DNS provider (if configured) creates external records
During Convergence #
Every convergence cycle includes a DNS reconciliation step:
- PSW builds the list of DNS records from the current project graph
- New apps get records created automatically
- Removed apps get records deleted automatically
- Existing records that already match are left untouched (idempotent)
When convergence creates a new target :
- DHCP provider writes a squat reservation on the target’s planned IP from
network.yml - The target boots with that planned IP statically wired into its network config — no DHCP request is ever made
Idempotency #
All provider operations are designed to be safe to repeat:
- Creating a DNS record that already exists → no change
- Removing a DNS record that doesn’t exist → no change
- Reserving an IP that’s already reserved for the same target → no change
This is critical because convergence runs every 5 minutes. Providers must handle being called repeatedly without creating duplicates or errors.
The “Managed by psw” Marker#
Every DHCP reservation and every DNS record PSW creates carries a marker — the literal text Managed by psw — at the start of its description (OPNsense) or comment (Cloudflare). PSW writes the marker on every create + update path, so:
- Anything with the marker was put there by PSW — safe for PSW to remove
- Anything without the marker was put there by you (or another tool) — PSW must never touch it
This is the invariant that makes orphan cleanup safe. If the marker ever drifted between write paths, PSW could either miss its own entries (leaving orphans behind) or delete entries you added by hand. So it lives in one shared helper (psw_lib.core.providers.markers) and every provider’s create/update code calls it.
Orphan Cleanup#
psw deploy reset already purges every PSW-managed DHCP reservation, Unbound DNS override, and Cloudflare DNS record for the project on disk — nothing PSW-marked is left behind under normal teardown. Orphans are what shows up when reset can’t run for the project: you wiped the project folder before reset, you cloned a fresh checkout on a new workstation, or a previous reset crashed mid-way.
psw provider clean --orphans is the project-less cleanup. It scans your providers for entries carrying the Managed by psw marker that don’t belong to any current project, and deletes them. It’s dry-run by default — pass --confirm to actually delete. You don’t need a project on disk to run it (which is the point — psw deploy reset needs a project, this doesn’t).
# Dry run — list orphans on both sides without deleting
psw provider clean --orphans \
--opnsense-url https://10.10.0.1 --opnsense-key-file ~/Downloads/opnsense-apikey.txt \
--cloudflare-token-file ~/Downloads/cf-token --domain yourdomain.ca
# Same command + --confirm to actually delete
psw provider clean --orphans \
--opnsense-url https://10.10.0.1 --opnsense-key-file ~/Downloads/opnsense-apikey.txt \
--cloudflare-token-file ~/Downloads/cf-token --domain yourdomain.ca \
--confirmPass only the credentials you have — each side is scanned only if its credentials are supplied.
Quick Reference#
| Provider | Required | OPNsense mode backend | ISP-router mode backend | Manages | Credentials In |
|---|---|---|---|---|---|
| Local DNS | Yes | OPNsense Unbound (recursive) | AdGuard Home + Unbound sidecar (recursive) | Internal DNS records + recursive resolution | opnsense / adguard_home |
| WAN DNS | No | Cloudflare | Cloudflare | External DNS records + ACME certs | cloudflare |
| DHCP | Yes | OPNsense Kea (squat reservations) | isp_router_partition (validation only — no router-side writes) | Either reservations or pool-collision validation | opnsense / none |