Skip to content

Latest commit

Β 

History

History
469 lines (337 loc) Β· 30.2 KB

README.md

File metadata and controls

469 lines (337 loc) Β· 30.2 KB

Statistics

Stats

Docker Logo


Docker

Docker Image Version Docker Image Size Docker Pulls Docker Stars

Github Logo

Maintenance Commit Activity Forks Stars Issues Pull Requests

Current Unbound release Current OpenSSL release

Table of Contents

What is Unbound

Unbound is a validating, recursive, caching DNS resolver. It is designed to be fast and lean and incorporates modern features based on open standards. Late 2019, Unbound has been rigorously audited, which means that the code base is more resilient than ever.

Source: unbound.net

About this Image

This advanced Unbound Docker image is based on Alpine Linux with focus on security, privacy, performance and a small size.

The Unbound process runs in the context of an unpriviledged non-root user, makes use of unprivileged ports (5335 tcp/udp) and the image is built using a secure distroless scratch image.

Unbound is configured as a DNSSEC validating DNS resolver, which directly queries DNS root servers utilizing zone transfers holding a local copy of the root zone (see IETF RFC 8806) as your own recursive upstream DNS server in combination with Pi-hole or AdGuard Home for adblocking in mind, but works also as a standalone server.

There's a really nice explanation at the Pi-hole documentation page of what that means without becoming too technical:

Whom can you trust? Recently, more and more small (and not so small) DNS upstream providers have appeared on the market, advertising free and private DNS service, but how can you know that they keep their promises? Right, you can't. Furthermore, from the point of an attacker, the DNS servers of larger providers are very worthwhile targets, as they only need to poison one DNS server, but millions of users might be affected. Instead of your bank's actual IP address, you could be sent to a phishing site hosted on some island. This scenario has already happened and it isn't unlikely to happen again... When you operate your own (tiny) recursive DNS server, then the likeliness of getting affected by such an attack is greatly reduced.

However, even though the image is intended to run a recursive setup, it does not necessarily mean that it has to be used that way. You are absolutely free to edit the unbound.conf file according to your own needs and requirements*, especially if you'd rather like to use an upstream DNS server which provides DoT or DoH features.

To provide always the latest stable, hardened and optimized versions per hardware architecture, the following software components are compiled online from source in the build processes of their own dedicated repositories using workflow driven CD pipelines using trusted GitHub Actions:

All components as well as the Internic files (root.hints and root.zone) are verified with their corresponding PGP keys and signature files if available to guarantee maximum security and trust.

When NLnet Labs publishes a new stable Unbound release, the image will be built, pushed to Docker Hub, tagged and released -including the required signing by the bot @madnuttah-bot according to the repo's strict security policies- to GitHub on a week-daily schedule without sacrificing security measures like SHA256 verification of the downloaded source tarball. As we take your network security serious, we are still able to manually update the image as soon as security fixes of the images' components were released. The same applies to the OpenSSL build environment when an OpenSSL update got released.

Note

We're not manually building release candidates of Unbound anymore, instead there are automated canary builds which will be created from the most recent NLnet Labs Unbound GitHub commit at 20:00 UTC from Monday to Friday if you want to ride on the bleeding edge of the development of Unbound.

The latest image is scanned for vulnerabilities using the Aqua Security Trivy and Docker Scout vulnerability scan on a daily schedule. If vulnerabilities have been detected, they'll show up in Security. The canary build only shows the results in the workflow's run details and are being scanned at buildtime. You need to be logged into GitHub to view the logs.

Installation

Distroless production and canary multiarch-builds for Linux-based 386, arm/v6, arm/v7, arm64 or amd64 platforms are available on Docker Hub.

How to use this Image

Please adapt the /usr/local/unbound/unbound.conf file and our example docker compose files to your needs. The docker compose files also deploy Pi-hole for blocking ads and to prevent tracking but isn't limited to Pi-hole. The image can also be used as an upstream for AdGuard Home or any similar ad blocking solution.

We don't like large, monolithic config files much.

Luckily Unbound can load configs through a include: clause. To provide a better structuring of the unbound.conf file, directories for optionally storing zone and other configuration files as well as for your certificates and the unbound.log file have been created and can be mounted as volumes:

Also make sure to uncomment the line include: "/usr/local/unbound/conf.d/*.conf" of the unbound.conf.

Warning

The config files in the conf.d and zones.d folders must be named with the suffix .conf to prevent issues with specific host configurations.

Don't forget to secure your setup when everything runs.

The splitted configuration files located in doc/examples/usr/local/unbound are only meant to give you an impression on how to separating and structuring the configs. Please mind that those files are examples which also needs to be edited to make them work for your environment if you intend to use them.

Note

Splitting ain't really necessary as the included default unbound.conf will perfectly do the job after you adapted the settings to suit your environment. You don't need to bind mount the config folders, just ignore them.

Directory Structure

Filesystem
/
β”œβ”€β”€ entrypoint
β”œβ”€β”€ usr
β”‚   β”œβ”€β”€ local
β”‚   β”‚   └── unbound
β”‚   β”‚       β”œβ”€β”€ certs.d
β”‚   β”‚       β”‚   └── ...
β”‚   β”‚       β”œβ”€β”€ conf.d
β”‚   β”‚       β”‚   └── *.conf
β”‚   β”‚       β”œβ”€β”€ iana.d
β”‚   β”‚       β”‚   β”œβ”€β”€ root.hints
β”‚   β”‚       β”‚   β”œβ”€β”€ root.key
β”‚   β”‚       β”‚   └── root.zone
β”‚   β”‚       β”œβ”€β”€ log.d
β”‚   β”‚       β”‚   └── unbound.log
β”‚   β”‚       β”œβ”€β”€ sbin
β”‚   β”‚       β”‚   β”œβ”€β”€ healthcheck.sh
β”‚   β”‚       β”‚   └── unbound.sh
β”‚   β”‚       β”œβ”€β”€ unbound.conf
β”‚   β”‚       β”œβ”€β”€ unbound.d
β”‚   β”‚       β”‚   β”œβ”€β”€ null -> /dev/null
β”‚   β”‚       β”‚   β”œβ”€β”€ random -> /dev/random
β”‚   β”‚       β”‚   β”œβ”€β”€ sbin
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ unbound
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ unbound-anchor
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ unbound-checkconf
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ unbound-control
β”‚   β”‚       β”‚   β”‚   β”œβ”€β”€ unbound-control-setup
β”‚   β”‚       β”‚   β”‚   └── unbound-host
β”‚   β”‚       β”‚   β”œβ”€β”€ unbound.pid
β”‚   β”‚       β”‚   └── urandom -> /dev/urandom
β”‚   β”‚       └── zones.d
β”‚   β”‚           └── *.conf
β”‚   β”œβ”€β”€ ...  
β”‚   β”‚
β”‚   ... 
...

Available Commands

Commands list
.                      fg                     sh
:                      getopts                shift
[                      grep                   source
[[                     groupmod               su-exec
alias                  hash                   sys/
awk                    healthcheck.sh         test
bg                     help                   times
bin/                   history                tini
break                  id                     trap
cd                     jobs                   true
chdir                  kill                   type
chgrp                  let                    ulimit
chown                  lib/                   umask
command                local                  unalias
continue               netstat                unbound
dev/                   printf                 unbound-anchor
drill                  proc/                  unbound-checkconf
echo                   pwd                    unbound-control
etc/                   read                   unbound-control-setup
eval                   readonly               unbound-host
exec                   return                 unset
exit                   sbin/                  usermod
export                 sed                    usr/
false                  set                    wait

Recommended Environment Variables

Variable Default Value Description
TZ UTC <Timezone> Set your local timezone as DNSSEC, logging and the optional redis and statistics rely on precise time

Optional Environment Variables

Variable Default Value Description
UNBOUND_UID 1000 INT Your desired user id for user _unbound
UNBOUND_GID 1000 INT Your desired group id for group _unbound
HEALTHCHECK_PORT 5335 INT The port Unbound uses (only used by the extended healthcheck)
EXTENDED_HEALTHCHECK false BOOL Set this to true if you want to use the extended healthcheck
EXTENDED_HEALTHCHECK_DOMAIN nlnetlabs.nl string The domain/host to run the extended healthcheck against
ENABLE_STATS false BOOL Set this to true if you want to enable Unbound statistics. Please follow the instructions there
DISABLE_SET_PERMS false BOOL Set this to true and define user _unbound for full rootless mode. The UNBOUND_UID and UNBOUND_GID will both be overridden with 1000 in that case

Caution

Setting DISABLE_SET_PERMS to true without defining user: _unbound or --user _unbound will run the container under the root account (which, of course, might be desired). The init screen in the log will show you the user who is running Unbound.

If you ain't sure what this variable does, you'll most likely don't need it.

Networking

Port Description
5335 Listening Port (tcp/udp)

Usage

The most elegant way to get started is using docker compose. We have provided combined Pi-hole/Unbound docker compose samples which we're using in slightly modified form that makes use of a MACVLAN/shim Bridge network which must be adapted to your network environment.

Important

All entries in angle brackets (<...>) need your very attention!

Note

You'll probably want an additional custom bridge network so your host is able communicate with the container and vice versa (for updating the Docker host, etc.). If you don't like to have an additional shim network, take a look at this workaround. We prefer using a combined MACVLAN/Bridge network configuration, but other network configurations will run as well.

If you don't want to run Unbound in recursive mode but rather having it forwarding your queries to an upstream server, make sure your ../iana.d/root.hints file is up-to-date and change the following lines in your unbound.conf:

server:
...
  for-upstream: no
...
forward-zone:
  name: "."
  # Forward queries to i.e. quad9
  forward-addr: 9.9.9.9
  forward-addr: 149.112.112.112
...

This image can also be used as a standalone DNS resolver without Pi-hole or AdGuard Home. The given ports must be changed to 53 (tcp/udp) in your unbound.conf and docker compose then. Additionally verify that connections to localhost are allowed (see healthcheck). You need to enable a capability in your compose file as the _unbound user only has limited permissions, see issue 54. You can find more information about runtime privileges and Linux capabilities here.

cap_add: 
  - NET_BIND_SERVICE

If you use a Synology Diskstation with Container Manager, take also a look here.

Anyway, you can also spin up the container with the following command:

docker run --name unbound -d \
  -p 5335:5335/udp \
  -p 5335:5335/tcp \
  --restart=unless-stopped \
  madnuttah/unbound:latest

CacheDB (Redis)

Tip

Even it takes a little more effort, we recommend accessing the CacheDB rather via Unix Socket than via tcp. The speed is superior in comparison to a network connection.

Due to the restricted environment of the image, it's not possible to just map and access the redis server's socket but need to use a "proxy" container which provides access to both containers, unbound as well as unbound-db, so there's an additional busybox container providing the socket in an own volume.

Extend your existing docker compose file's server: section with the content of this snippet. The loading order of the modules is also important, cachedb has to be loaded before iterator and after validator.

server:
  module-config: "validator cachedb iterator"

Create a new mountpoint like ../unbound-db/, and place this redis.conf there.

Create a new entry for cachedb in your unbound.conf with the content of this cachedb.conf or put the file in your conf.d directory if you use the structured directories.

You can verify the connection to redis in the unbound.log or by typing docker logs unbound in the shell:

...
Feb 18 22:01:02 unbound[1:0] notice: init module 1: cachedb
Feb 18 22:01:02 unbound[1:0] notice: Redis initializationΒ 
Feb 18 22:01:02 unbound[1:0] notice: Connection to Redis established
...

If you like to have a healtheck for this container which we'd recommend strongly, you got our back. Read on, we'll explain how to set this up in the next heading.

In Portainer you can also view the cachedb.d volume with a contained redis.sock file by clicking browse.

image

Another way is to connect to the unbound-db container and monitor the redis-cli:

Socket:

docker exec -ti unbound-db sh -c "redis-cli -s /usr/local/unbound/cachedb.d/redis.sock monitor"

Network:

docker exec -ti unbound-db sh -c "redis-cli monitor"

The boot order set in your docker compose is also important. Redis depends on the 'socket server', Unbound depends on Redis. If you use Pi-hole or AdGuard Home, it will depend on Unbound.

For the sake of completeness, you can also flange Unbound to Redis by network. Just follow the given steps, except the part preparing and creating the unbound-db-socket container. Use proper name resolution by whether docker internal resolution, your DNS or use fixed IP addresses and change your cachedb.conf or your unbound.conf according to this:

cachedb:
  backend: "redis"
  redis-server-host: unbound-db # The hostname or IP of your Redis server
  redis-server-port: 6379
  redis-expire-records: yes

Healthcheck

Important

The general use of the healthcheck is optional but highly recommended.

The healthcheck can be enabled and configured quite self-explanatory in your docker compose file. Check out the example compose files or the snippet below to get you started; each compose file has got the healthcheck included, the most complete example is the one we use ourselves.

  ...
  unbound:
  ...
    healthcheck:
      test: /usr/local/unbound/sbin/healthcheck.sh
      interval: 60s
      retries: 3
      start_period: 5s
      timeout: 15s
  ...

To enable the healthcheck for your CacheDB (Redis) server, please define it in your docker compose file according to this and download it's healthcheck script and put it in it's persistent volume, make it available in the compose file's volume definition and make the file executable. You will need to restart the Redis container afterwards.

The default healthcheck only checks for opened Unbound ports using netstat and grep. We got asked why we don't include netcat (nc) into the image to actually connect to opened ports, this is the reason.

To enable the extended healthcheck, which uses NLnet Labs' LDNS drill tool to query domains or hosts, please set the healtheck's optional environment variables in your docker compose file or run command.

Note

The extended healthcheck is deactivated by default in favor of your privacy and security.

To verify that the healthcheck is working and the container is doing what it is supposed to do, consult your Portainer instance or execute docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh in the shell of your Docker host.

Standard healthcheck console output:

yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
βœ… Port 5335 open

Standard healthcheck console output showing an issue:

yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
⚠️ Port 5335 not open

Extended healthcheck console output:

yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
βœ… Port 5335 open
βœ… Domain 'unbound.net' resolved to '185.49.140.10'

Extended healthcheck console output showing an issue:

yourdockerhost:~# docker exec -ti unbound /usr/local/unbound/sbin/healthcheck.sh
 βœ… Port 5335 open
 ⚠️ Domain 'unbound.net' not resolved

Not in the console but rather in Portainer (and here on this page of course) the colored unicode emoji icons will show you the condition of your container at a first glance.

Updating the Image

Warning

Even we use it by ourselves to keep less important containers updated, we don't recommend using solutions like Watchtower to update critical services like your production DNS infrastructure automatically.

A notification service like the one in Watchtower or DIUN can inform you when an update has been released so you can take appropriate action if needed.

If you want to manually update to the latest version available on Docker Hub, just pull the image using docker compose pull and recreate your container by executing docker compose up -d.

Pulling the latest image without a compose file can be done by docker pull madnuttah/unbound:latest.

Unbound Statistics

Image

We also created a companion project using Zabbix for shipping the Unbound stats via a Zabbix Active Agent to Grafana without using additional tools like Zabbix Sender using a frankensteined healthcheck script.

Known Issues

  • There's a difference between 'vanilla' Docker and the variant Synology uses. If the container won't spin up when trying to use a privileged port like 53 tcp/udp you might need to run the container in root mode by setting the DISABLE_SET_PERMS environment variable to true without a user definition or define user: root in the compose file's Unbound service section or your shell command.

Troubleshooting

Tip

You can run the available commands directly from the shell of your Docker host like docker exec -ti unbound COMMAND. To check your Unbound config(s) for errors for example, execute docker exec -ti unbound unbound-checkconf.

  • You'd like to use a different unbound.conf than the one included? No problem, just make sure to change at least the following settings and fix crucial paths, otherwise the container will fail to start:
server:
   username="" # Not set in config but compose or command if needed
   chroot="" # Distroless, so no chroot necessary
   directory="/usr/local/unbound" # This is the folder where Unbound lives in
  • If you have trouble spinning up the container, start it with the minimal config first. Analyze the logs using docker logs unbound or your unbound.log and fix warnings and errors there. When it runs, attach volumes one by one. Success means to adapt the default unbound.conf to your needs then.

  • Most issues take place because there are missing files like the unbound.log or due to incorrect permissions on Unbound's volumes. The container won't start up in such cases. Make sure your UNBOUND_UID/UNBOUND_GID, default: 1000:1000, (_unbound:_unbound) has read/write permissions on it's folders.

  • When you see log entries like those below or similar issues in the healthcheck, verify that you permit connections to localhost. There are multiple places in the unbound.conf where this could be disabled, from access control to Do-Not-Query-Localhost and so on. You'll likely need to check the whole file. Isn't that alone one good reason for the concept with the separated config directories? If you can't find the culprit, don't hesitate giving us a shout.

unbound[1:0] fatal error: could not open ports
unbound[1:0] error: can't bind socket: Permission denied for 127.0.0.1 port 53 
  • If you see the warning unbound[1:0] warning: unbound is already running as pid 1, executing docker compose down && docker compose up -d will remove the PID and also the warnings in the log.

  • This is no issue and shows that Unbound is doing trust anchor signaling to the root name servers. See this URL for more details.

unbound[0:1] info: generate keytag query _ta-4f66. NULL IN

You'll find a redacted version of the Docker compose stack we`re currently using for comparison purposes here.

Documentation

In-depth documentation for NLnet Labs Unbound is available on the Unbound documentation website and here goes a direct link to the documentation of the default unbound.conf file.

Contributing

You have found a bug, got something to make better, have an idea for a shiny new feature or just a question? That's amazing! Feel free to submit an issue or a pull request, we ❀️ contributions by the open source community.