Managing Unbound DNS overrides with Boundation
Boundation GitHub repository
At some point in the past six months or so I’ve started using a mini pc with OPNsense installed as my router. I’ve replaced AdGuard Home with Unbound DNS running on the router. It provides all the blocklist functionality I want, plus Unbound integrates quite well with the DHCP server.
Generally I like Unbound, but managing DNS changes automatically has proven more challenging than with AdGuard Home. With AdGuard home I used ExternalDNS and the External DNS - Adguard Home Provider (Webhook) to automatically manage DNS entries for my Kubernetes deployment. No such option seems to exist for Unbound.
So I built Boundation. That’s the best name I could come up with. Let’s not talk about that.
Requirements
- DNS entries must be created for environments as they’re spun up. For example, if I open a PR for one of my web projects, a test environment is created with a new ingress. My internal DNS must be updated with the appropriate records.
- DNS entries must be removed when environments are destroyed. When I merge the example PR, the test environment is automatically torn down. The DNS record must also be cleaned up.
- Bonus: Ability to manage DNS for services outside of Kubernetes.
Initial Solution
I created an External DNS Webhook for Unbound using the OPNsense API. When I deployed it with External DNS, I found that duplicate DNS entries were constantly being created. I had either misconfigured External DNS, or done something wrong. Either way, I realized External DNS was overkill. The only time I need a DNS entry to change is when I Create, Update, or Delete a deployment.
Simplified Solution
I took the work I’d done for the webhook and created a simple CLI. Some simple usage examples:
Create or Update overrides:
unbound upsert --host=example.domain.here --target=1.2.3.4 --host=other.host.com --target=5.6.7.8
Read existing overrides
unbound read
Delete overrides
unbound delete --host=example.domain.here
Run interactive configuration menu
unbound configure
Source code for both solutions can be found in the boundation repo.
Example snippets for GitLab CI
I use GitLab for my internal projects, so here’s an example .gitlab-ci.yml
snippet for pull request deployments, creating the DNS entry then deleting the entry when the environment is stopped.
pr-deploy:
rules:
- if: $CI_MERGE_REQUEST_ID
stage: deploy
needs:
- jobs
- needed
- for
- deploy
- here
variables:
ENV_HOSTNAME: ${CI_MERGE_REQUEST_ID}-example.domain.here
image:
name: alpine/k8s:1.25.13
entrypoint: [""]
script:
- deployment_command_here.sh
environment:
name: ${CI_MERGE_REQUEST_ID}
url: https://${ENV_HOSTNAME}
on_stop: pr-stop
pr-dns-create:
needs:
- pr-deploy
rules:
- if: $CI_MERGE_REQUEST_ID
stage: dns
variables:
ENV_HOSTNAME: ${CI_MERGE_REQUEST_ID}-example.domain.here
TARGET_IP: 1.2.3.4 # put the ip address you want here
script:
- go install github.com/MrUsefull/boundation/cmd/unbound@latest
- mv ${UNBOUND_CFG} ${UNBOUND_CFG}.yaml # current cfg package needs a .yaml extension. May change.
- unbound upsert --host=${ENV_HOSTNAME} --target=${TARGET_IP} --config=${UNBOUND_CFG}.yaml
pr-stop:
stage: deploy
image:
name: alpine/k8s:1.25.13
entrypoint: [""]
environment:
name: ${CI_MERGE_REQUEST_ID}
action: stop
rules:
- if: $CI_MERGE_REQUEST_ID
when: manual
script:
- stop_command_here.sh
pr-dns-delete:
needs:
- pr-stop
stage: dns
environment:
name: ${CI_MERGE_REQUEST_ID}
action: stop
rules:
- if: $CI_MERGE_REQUEST_ID
variables:
ENV_HOSTNAME: ${CI_MERGE_REQUEST_ID}-example.domain.here
script:
- go install github.com/MrUsefull/boundation/cmd/unbound@latest
- mv ${UNBOUND_CFG} ${UNBOUND_CFG}.yaml # current cfg package needs a .yaml extension. May change.
- unbound delete --host=${ENV_HOSTNAME} --config=${UNBOUND_CFG}.yaml
Example cli config. You can get the API key in the OPNsense UI under System
-> Access
-> Users
-> ${USER}
-> click the +
button in the API Keys
section.
opnsense:
baseurl: https://router.example.domain.here
creds: <API_KEY_NAME>:<API_KEY_SECRET>
filter:
filter: []
exclude: []
loglevel: INFO
Obviously the script sections above can be pulled into more reusable scripts, this is just an example afterall. The snippet above is 90% of a fully automated environment deployment and DNS management with GitLab CI.