Securing your IPv6-only docker server

It is important to ensure your IPv6-only docker server is secure.

First configure your firewall to allow secure shell (SSH), port 22, so that you can maintain your remote connection.

Then turn on your firewall with default deny incoming and default deny routing rules.

This ensures your server is secure-by-default, and only then should you allow routing to the specific containers and ports that you want to expose.

My server runs Ubuntu, so these instructions are based on the Uncompliciated Firewall (UFW), but similar considerations apply to other platforms

Enabling security-by-default

If you enabled the UFW firewall on a remote server without any configuration it would deny all incoming connections — including the SSH connection you are using to configure it.

So, first of all, ensure you allow SSH to continue working:

sudo ufw allow to ::/0 port ssh

If you are using an alternative port for SSH then make sure you enable that instead:

sudo ufw allow to ::/0 port 2222 proto tcp

Once this is configured, enable the default UFW firewall and check the status

sudo ufw enable
sudo ufw status verbose

You should see output similar to:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (v6)                ALLOW IN    Anywhere (v6)

This will make your server secure by default, with all incoming connections, and any forwarding connections to docker, denied by default, unless you explicitly allow them.

You probably want to turn on the firewall before you configure the rest of the server, especially if something goes wrong in configuration, e.g. you lose SSH access, then you may have to rebuild the server.

Note: If you use the simplified syntax sudo ufw allow ssh on an IPv6 only server it will work, just it will create a bunch of unnecessary IPv4 rules.

Why do you need to enable this on IPv6?

With IPv6 you will have routeable addresses for all your docker services, including internal databases and other containers. By default you want them able to connect to each other, and make outgoing connections to the Internet, but only allow incoming connections when explicitly permitted.

On a home router, this will be the default configuration, with the router blocking all traffic unless explicity allowed. A default server installation may not have this configured, which is why it is important to check and, after allowing SSH, make sure the server is secure.

If you already have an external firewall protecting the zone that your IPv6-only server is in, then you may not need a local server firewall (especially if you want it open to other servers in the zone). However 'defense in depth' is a good security practice, so you may still want to secure individual servers.

If you server is running at a general hosting company, with full Internet access, and no external firewall, then running a firewall on the server is important for security.

What about IPv4?

With IPv4 you don't have routable addresses, you are using private address ranges, which might be routable to a larger 10.x network if you have one configured, but are not routable to the Internet.

You need to explicitly allow incoming Network Address Translation (aka Port Forwards), for any incoming traffic, which by coincidence also gives you security by default.

The best practice with IPv6 is to configure similar, and have 'default deny incoming', so only the traffic you want is allowed through.

Allowing traffic for initial testing

Outbound traffic is allowed by default, so test connections made from your host will work.

As part of Setting up your IPv6-only docker host, you will have enabled NAT66 for outbound connections, which use your host, so outbound connections from containers, even those without public addresses, will work:

docker exec demo-busybox-1 ping6 -c -4 google.com

However to allow incoming traffic you will need to add the appropriate rules to UFW.

Container ports

During setup you will have enabled IPv6 fowarding, and set up the Neighbour Discovery Protocol Proxy Daemon, to advertise the public addresses of the docker containers.

This will allow you to connect directly to incoming containers, routed via your host, from the external network to the docker public network.

To allow this, you need to add a UFW rule to allow forwarding to the container public address on the port used on the container (not the mapped port):

sudo ufw route allow to 2001:db8:1234:a1:1::ab53 port http

Once UFW is open (and it is also allowed through any external firewall), you can connect directly to the container:

http://[2001:db8:1234:a1:1::ab53]

Published ports

Published ports are opened on the server itself, so need to be matched with an allow rule (not routing) in UFW.

To allow access to the published port 8081:

sudo ufw allow to ::/0 port 8081 proto tcp

Once this is allowed in UFW, you will be able to test inbound connections to the published port on the host.

http://[2001:db8:1234:a1::1]:8081

Container ports vs published ports

You need to understand the difference between contaner ports and published ports.

The relevant parts of the busybox configuration in compose.yml, from the server setup example, are:

busybox:
  ...
  networks:
    ...
    public:
      ipv6_address: '2001:db8:1234:a1:1::ab53'
  ports:
    - '8081:80'

Port 80 is the port on the container address, '2001:db8:1234:a1:1::ab53', which is accessed directly via the route (forward) UFW rule.

In the port mapping, the left hand port 8081 is the port on the host that is also mapped to the same container port; this is published port on the host, and accessed via the allow rule in UFW.

You can check the two rules in UFW:

sudo ufw status verbose

The separate routing (FWD, port 80), and incoming (IN, port 8081) can be seen:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (v6)                ALLOW IN    Anywhere (v6)
8081/tcp (v6)              ALLOW IN    Anywhere (v6)
2001:db8:1234:a1:1::ab53 80/tcp ALLOW FWD   Anywhere (v6)

You don't need both, they are just two different ways to access the same container (see below).

Warning: Container ports are exposed even if they are not published

Container ports are accessible even if they are not published!

In the above example, the container port, 80 is also published on 8081, however even if a port is not published it can still be accessed, if it is allowed through the firewall.

You can confirm this by allowing access to another public address and creating a test service without published ports:

sudo ufw route allow to 2001:db8:1234:a1:1::64fd port http
docker run --rm -d --name test64fd --network=demo_public \
  --ip6="2001:db8:1234:a1:1::64fd" busybox \
  sh -c 'echo "Hello world!" > index.html && httpd -f -v'

You will be able to browse to http://[2001:db8:1234:a1:1::64fd]/ even though you can check with docker ps that the container has no published ports.

After testing, clean up by removing the container and rule:

docker stop test64fd
sudo ufw route delete allow to 2001:db8:1234:a1:1::64fd port http

This is why it is important to be secure by default with IPv6 and ensure that there is a default deny incoming rule, otherwise you could allow unintended access to container ports.

Having a firewall is part of defense-in-depth. Even without one, an internal database will still require a login and password, an internal mail server will usually only relay traffic from internal addresses, and accessing a server over HTTP is only a risk if you then use a password.

Services are likely protected even if they are accessible; adding a firewall provide extra protection, for example if a new security flaw becomes known it can't be exploited if the server can't be accessed.

Benefits

Like your home gateway IPv6 firewall (or like IPv4 port mapping/NAT), you want the default to be no access, only only allow access as needed.

The benefits of running IPv6 containers is that each container can have it's own address, and you can expose that container directly using Uncomplicated Firewall Rules without having to do port mapping.

This means if you have two containers that both want to run on port 443 and 80, then each can run on it's own container address, with the right firewall rules in place, and without having to map one of them like 80->8080, 443->8443.

You may still want to use a proxy container, such as Caddy, that will automatically handle HTTPS certificates for you for some services, and you can expose port 443 and 80 on the Caddy container directly via a routing (forwarding rule), rather than have to map them.

e.g. if the Caddy container is running on 2001:db8:1234:a1:1::cd4e, then you will want to expose port 443 and 80 for this address, and point your DNS host names at the same address.

sudo ufw route allow to 2001:db8:1234:a1:1::cd4e port http
sudo ufw route allow to 2001:db8:1234:a1:1::cd4e port https

Requests will be routed to the Caddy container (allowed through the firewall), and it can then configure automatic HTTPS certificates using both port 443 and 80.

The Caddy container can then pass the unencrypted requrests to the destination containers, on their port 80 (or whatever they use), yet direct access to those other container will be blocked by the firewall.

You can run the Caddy proxy on ports 443 and 80 on one container address, and then also allow access directly to a different service on the same ports, but on a different container address..

Secure by default (deny incoming, and forwarding), then allow routing (forwarding) to only the containers you want, only on the ports they are using.

Leave a Reply

Your email address will not be published.