A Salt formula to install and configure the Unbound recursive DNS resolver.
Note
See the full Salt Formulas installation and usage instructions.
Table of Contents
- Salt >= 3006 (tested on 3006+)
- Python >= 3.6 (Salt dependency)
- For integration testing: Ruby >= 3.0, Bundler, Docker
Add the formula to your Salt master /etc/salt/master:
fileserver_backend:
- gitfs
- roots
gitfs_remotes:
- https://github.com/SomeBlackMagic/salt-unbound-formula.gitClone the repository into your file_roots:
git clone https://github.com/SomeBlackMagic/salt-unbound-formula.git /srv/salt/unbound-formulaThen symlink (or add /srv/salt/unbound-formula to file_roots) so the unbound directory
is reachable as salt://unbound.
Meta-state. Installs the unbound package, deploys configuration, and ensures the service is running.
Installs the unbound package and downloads the DNS root hints file
(named.cache) from https://www.internic.net/domain/named.cache.
Manages /etc/unbound/unbound.conf (path varies by OS) using Pillar data
and the map.jinja lookup table.
Ensures the unbound service is enabled and running. On Debian/Ubuntu,
also manages /etc/default/unbound.
The following platforms are actively tested via Test Kitchen (see Testing):
| Platform | Image |
|---|---|
| Debian 12 | debian-12 |
| Ubuntu 22.04 | ubuntu-2204 |
| Ubuntu 24.04 | ubuntu-2404 |
| Rocky Linux 9 | rockylinux-9 |
| AlmaLinux 9 | almalinux-9 |
| CentOS Stream 9 | centos-stream-9 |
| Amazon Linux 2023 | amazonlinux-2023 |
| openSUSE Leap 15 | opensuse-leap-15 |
The following platforms are supported by the OS map but not currently included in the automated test suite:
| OS Family | Examples |
|---|---|
| FreeBSD | FreeBSD |
| Arch | Arch Linux |
| Gentoo | Gentoo |
| OpenBSD | OpenBSD |
This formula is configured via Salt Pillar. Copy pillar.example to your
Pillar tree and adjust as needed.
All options are nested under the unbound key. Lookup overrides (package
name, file paths) go under unbound:lookup.
unbound:
server:
interfaces:
- 127.0.0.1
- ::1
acls:
- 127.0.0.0/8 allow
- 10.0.0.0/8 allow
forwardzones:
- zonename: "."
options:
- key: forward-addr
value: 9.9.9.9Stub zones forward queries for a specific domain to an authoritative name server:
unbound:
stubzones:
- zonename: example.lan
options:
- key: stub-addr
value: 192.168.1.1
- key: stub-addr
value: 192.168.1.2unbound:
server:
# Disable DNSSEC validation for these domains
domains_insecure:
- example.lan
# Allow private address responses for these domains
private_domains:
- example.lan
# Block rebinding attacks — these addresses are treated as private
private_addresses:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 169.254.0.0/16
- fd00::/8
- fe80::/10
# Local zone declarations
local_zones:
- example.lan staticunbound:
remote:
options:
- key: control-enable
value: "yes"
- key: control-interface
value: 127.0.0.1Any unbound.conf server: directive can be passed as a key/value pair:
unbound:
server:
options:
- key: verbosity
value: 1
- key: use-syslog
value: "yes"
- key: hide-identity
value: "yes"
- key: hide-version
value: "yes"
- key: harden-glue
value: "yes"
- key: harden-dnssec-stripped
value: "yes"
- key: use-caps-for-id
value: "yes"
- key: num-threads
value: 4
- key: rrset-cache-size
value: 256m
- key: msg-cache-size
value: 128mOverride any lookup value (package name, file paths, etc.):
unbound:
lookup:
config_file: /usr/local/etc/unbound/unbound.conf
cache_file: /usr/local/etc/unbound/named.cacheSee pillar.example for the full list of available options with comments.
| Key | Default (Debian) | Description |
|---|---|---|
| package | unbound | Package name to install |
| service | unbound | Service name |
| config_file | /etc/unbound/unbound.conf | Path to main config file | |
| cache_file | /etc/unbound/named.cache | Path for root hints file | |
| config_group | root | Group owner of the config file |
| defaults_file | /etc/default/unbound | Debian/Ubuntu only: defaults file |
| Key | Type | Description |
|---|---|---|
| interfaces | list of strings | Addresses to listen on |
| acls | list of strings | Access control rules (CIDR + action) |
| options | list of {key, value} maps |
Raw server: directives |
| domains_insecure | list of strings | Domains exempt from DNSSEC validation |
| private_domains | list of strings | Domains allowed to return private IPs |
| private_addresses | list of strings | CIDR ranges treated as private |
| local_zones | list of strings | Local zone declarations |
Both accept a list of zone definitions:
- zonename: <string> # required
options:
- key: <string>
value: <string>remote:
options:
- key: control-enable
value: "yes"This formula uses Test Kitchen with the kitchen-salt provisioner and InSpec for verification.
Install Ruby gem dependencies:
make install
# equivalent to: bundle config set --local path vendor/bundle && bundle installmake list # list all Kitchen instances
make test # full test cycle (converge + verify + destroy) for all platforms
make converge # converge all platforms
make verify # verify all platforms
make destroy # destroy all instances
# Per-platform targets
make test-debian-12
make test-ubuntu-2204
make test-ubuntu-2404
make test-rockylinux-9
make test-amazonlinux-2023
# Open a shell in a running instance
make login PLATFORM=debian-12bundle exec kitchen list
bundle exec kitchen test default-debian-12
bundle exec kitchen converge default-ubuntu-2404
bundle exec kitchen verify default-rockylinux-9
bundle exec kitchen login default-debian-12Validate syntax (Jinja + YAML + state structure):
salt '*' state.show_sls unboundApply the formula:
salt '*' state.apply unboundDry-run mode:
salt '*' state.apply unbound test=True- Fork the repository on GitHub.
- Create a feature branch from
master. - Add or update tests in
test/integration/default/controls/. - Run
make testlocally against the affected platforms. - Submit a pull request with a clear description of the change.
Please keep changes focused — one logical change per PR. Update CHANGELOG.rst
for any user-visible change.
See LICENSE.