How to use `iptables` inside Discourse docker container

How can I configure iptables in the Discourse docker container?

I specifically need the firewall controls inside the docker container because I want to be able to add conditional rules based on user id.

I was first just denying any internet access to the docker container entirely (this is do-able if you use web.socketed.template.yml because nginx will communicate over unix domain sockets instead of the network) via dockerd ... --iptables=false

[root@osestaging1 templates]# grep ExecStart /usr/lib/systemd/system/docker.service
#ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --iptables=false
[root@osestaging1 templates]# 

…but I realized that the docker container should probably at least have internet access so it can update its base OS (currently Debian 10) with critical security updates via unattended-upgrades. So I’d rather just grant root & apt Internet access and deny all else via iptables running inside the Discourse docker container.

I set this up. The most complicated part was that–even if you’re root–you’ll still get “permission denied” errors when attempting to view or edit iptables rules inside of the default Discourse docker container.

root@24a1f9f4c038:/# iptables -L
# Warning: iptables-legacy tables present, use iptables-legacy to see them
iptables: Permission denied (you must be root).
root@24a1f9f4c038:/# 

This is because–out of the box–the Discourse launcher script that wraps the docker commands runs the Discourse docker container without the “NET_ADMIN” capability.

The most robust way to add the NET_ADMIN capability to the Discourse docker container is to update your container’s yaml file to include the necessary argument to the docker run ... /sbin/boot command via the docker_args yaml string:

docker_args: "--cap-add NET_ADMIN"

For example:

[root@osestaging1 discourse]# head -n15 containers/app.yml
## this is the all-in-one, standalone Discourse Docker container template
##
## After making changes to this file, you MUST rebuild
## /var/discourse/launcher rebuild app
##
## BE *VERY* CAREFUL WHEN EDITING!
## YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
## visit http://www.yamllint.com/ to validate this file as needed

docker_args: "--cap-add NET_ADMIN"

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
[root@osestaging1 discourse]# 

These will get added to the docker run ... /sbin/boot command executed by launcher via the $user_args variable:

[root@osestaging1 discourse]# grep -A2 -E '^\s*\$docker_path run' launcher
     $docker_path run --shm-size=512m $links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
        -e DOCKER_HOST_IP="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
        $run_image $boot_command
[root@osestaging1 discourse]# 

Because it’s super complex to make changes baked-into the discourse docker image after a docker pull and before the docker run ... /sbin/boot, I decided to have iptables installed & configured via a simple script executed by runit when the container boots.

The following command will create the necessary template yaml file which will create the runit script on the next ./launcher rebuild app

cat << EOF > /var/discourse/templates/iptables.template.yml
run:
  - file:
     path: /etc/runit/1.d/01-iptables
     chmod: "+x"
     contents: |
        #!/bin/bash
        ################################################################################
        # File:    /etc/runit/1.d/01-iptables
        # Version: 0.2
        # Purpose: installs & locks-down iptables
        # Author:  Michael Altfield <michael@opensourceecology.org>
        # Created: 2019-11-26
        # Updated: 2019-12-17
        ################################################################################
        sudo apt-get update
        sudo apt-get install -y iptables 
        sudo iptables -A INPUT -i lo -j ACCEPT
        sudo iptables -A INPUT -s 127.0.0.1/32 -j DROP
        sudo iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo iptables -A INPUT -j DROP
        sudo iptables -A OUTPUT -s 127.0.0.1/32 -d 127.0.0.1/32 -j ACCEPT
        sudo iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo iptables -A OUTPUT -m owner --uid-owner 0 -j ACCEPT
        sudo iptables -A OUTPUT -m owner --uid-owner 100 -j ACCEPT
        sudo iptables -A OUTPUT -j DROP
        sudo ip6tables -A INPUT -i lo -j ACCEPT
        sudo ip6tables -A INPUT -s ::1/128 -j DROP
        sudo ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo ip6tables -A INPUT -j DROP
        sudo ip6tables -A OUTPUT -s ::1/128 -d ::1/128 -j ACCEPT
        sudo ip6tables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo ip6tables -A OUTPUT -m owner --uid-owner 0 -j ACCEPT
        sudo ip6tables -A OUTPUT -m owner --uid-owner 100 -j ACCEPT
        sudo ip6tables -A OUTPUT -j DROP

EOF

Note that this is a very basic iptables config. With the web.socketed.template.yml file (which I use), the docker container technically doesn’t need Internet access at all since the nginx reverse proxy on the docker host communicates through to the Discourse nginx on the docker container via a unix domain socket. The main reason I permit Internet access is so that the docker container’s base OS can update itself with critical security patches via unattended-upgrades. Hence opening for user 100, which is the apt user.

Finally, add the above templates/iptables.template.yml file to your container’s app.yaml file.

[root@osestaging1 discourse]# head -n15 /var/discourse/containers/discourse_ose.yml
## this is the all-in-one, standalone Discourse Docker container template
##
## After making changes to this file, you MUST rebuild
## /var/discourse/launcher rebuild app
##
## BE *VERY* CAREFUL WHEN EDITING!
## YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
## visit http://www.yamllint.com/ to validate this file as needed

docker_args: "--cap-add NET_ADMIN"

templates:
  - "templates/iptables.template.yml"
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
[root@osestaging1 discourse]# 

Now you should be able to

./launcher rebuild app

And after waiting about 10 minutes, your Discourse docker container will have iptables fully functioning :slight_smile:

1 Like

:warning: Whoever you are, whatever you do, please don’t try this at home, school or anywhere!

I’m quite sure that you are crippling your Discourse instance by disallowing the unicorn and sidekiq processes (owned by the discourse user) to access the internet. But yeah, I’m sure you know what you are doing.

14 Likes

What problem is this solving?

4 Likes