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:

  1. The target running Jellyfin needs a stable IP address (planned in network.yml, protected by a DHCP — Dynamic Host Configuration Protocol — squat reservation)
  2. The domain jellyfin.yourdomain.ca needs to resolve to the right IP on your local network (local DNS)
  3. If you want to access it from outside your home, the domain needs a public DNS record too (WAN DNS)
  4. 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.ca10.10.0.100 (your internal Traefik IP)
  • On the internet: jellyfin.yourdomain.ca → your public IP or tunnel address
Required?Yes
Backendsopnsense_unbound (preferred when you have OPNsense) · adguard_home (when you don’t)
What it managesInternal 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 backendCloudflare
What it managesPublic 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
Backendsopnsense_kea (when OPNsense runs your DHCP) · isp_router_partition (when your ISP/home router does)
What it managesEither 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:

  1. Create a temporary DNS record for the challenge
  2. Let’s Encrypt verifies the record exists
  3. Let’s Encrypt issues the certificate
  4. 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: dns

How 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-kea

When 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_kea

secrets/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 #

  1. DHCP provider reserves an IP for the bootstrap target
  2. Core apps are deployed
  3. Local DNS provider creates internal records for all core apps (e.g., dashboard.yourdomain.ca → Traefik IP)
  4. WAN DNS provider (if configured) creates external records

During Convergence #

Every convergence cycle includes a DNS reconciliation step:

  1. PSW builds the list of DNS records from the current project graph
  2. New apps get records created automatically
  3. Removed apps get records deleted automatically
  4. Existing records that already match are left untouched (idempotent)

When convergence creates a new target :

  1. DHCP provider writes a squat reservation on the target’s planned IP from network.yml
  2. 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 \
  --confirm

Pass only the credentials you have — each side is scanned only if its credentials are supplied.

Quick Reference#

ProviderRequiredOPNsense mode backendISP-router mode backendManagesCredentials In
Local DNSYesOPNsense Unbound (recursive)AdGuard Home + Unbound sidecar (recursive)Internal DNS records + recursive resolutionopnsense / adguard_home
WAN DNSNoCloudflareCloudflareExternal DNS records + ACME certscloudflare
DHCPYesOPNsense Kea (squat reservations)isp_router_partition (validation only — no router-side writes)Either reservations or pool-collision validationopnsense / none