Securing a Web Server Using a Linux Namespaces Sandbox

The goal of this article is to isolate a small public web server on a simulated demilitarized zone (DMZ) network, and to restrict the local network access in case the server is breached. It is an extra security layer added to an existing home server setup.

Internal DMZ network setup

Internal DMZ network setup

The DMZ consists of an internal network 10.10.20.0/24 connected to br0 bridge device. On this network I place a Linux namespaces security sandbox at 10.10.20.10, running a web server. In case an intruder gets control of the web server, he will be running with low privileges as a generic www-data user. The host firewall configuration will not allow him to open connections anywhere outside DMZ network.

To build the sandbox I use Firejail on a Debian 7 computer. For any other Linux distribution the setup steps are similar. All the commands specified below are executed as root.

Step 1: Install Firejail

The download page provides source code (./configure && make && sudo make install), deb (dpkg -i firejail.deb) and rpm (rpm -i firejail.rpm) packages. The project page also lists an Arch Linux package. The software has virtually no dependencies and it will work with any 3.x Linux kernel.

Step 2: Install nginx web server

Install nginx or any other web server you are familiar with. Stop the server and make sure it is not started by default at power up:

# apt-get install nginx
# /etc/init.d/nginx stop
Stopping nginx: nginx.
# insserv -r nginx

On Debian and Ubuntu, nginx serves pages from /usr/share/nginx/www directory. The logs are stored in /var/log/nginx/.

Step 3: Configure 10.10.20.0/24 network

Create and configure the internal 10.10.20.0/24 network. The host interface br0 has the address 10.10.20.1:

# apt-get install bridge-utils
# brctl addbr br0
# ifconfig br0 10.10.20.1/24

You should have in this moment the bridge interface up an running:

# ifconfig br0
br0       Link encap:Ethernet  HWaddr e6:55:ca:1c:29:4a  
          inet addr:10.10.20.1  Bcast:10.10.20.255  Mask:255.255.255.0
          inet6 addr: fe80::e455:caff:fe1c:294a/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:0 (0.0 B)  TX bytes:468 (468.0 B)

Step 4: Host firewall configuration

Forward TCP port 80 on the host to TCP port 80 in sandbox, and drop all traffic originated on DMZ network. Also, enable routing on the host. My iptables script is as follows:

#!/bin/bash
# netfilter cleanup
iptables --flush
iptables -t nat -F
iptables -X
iptables -Z
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT

# enable ipv4 forwarding
echo "1" > /proc/sys/net/ipv4/ip_forward

# forward host tcp port 80 to our sandbox
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to 10.10.20.10:80

# drop any traffic originated on 10.10.20.0/24 network
iptables -A FORWARD -i br0 -m state --state NEW,INVALID -j DROP
iptables -A INPUT -i br0 -m state --state NEW,INVALID -j DROP
 

Step 5: Start the sandbox

Start the server using Firejail:

# firejail --private --seccomp --net=br0 --ip=10.10.20.10 \
"/etc/init.d/rsyslog start; \
/etc/init.d/nginx start; \
sleep inf" &

The command configures the bridge device and the IP address. It starts a syslog server and nginx inside the sandbox. Use sleep inf to keep the session open indefinitely.

Sandbox isolation:

  • Filesystem: The sandbox has isolated the filesystem by making all directories read-only. Option –private removes /root, /home and /tmp directories. Among other things, this also isolates X11 socket and prevents any kind of snooping on X11 sessions.
  • Process space: The only processes visible in the sandbox are the processes started in the sandbox (syslog and web server). This prevents attacks such as strace attack. You can get a list of the processes running in sandbox using firejail –list command:
    # firejail --list
    3867:root:firejail --private --seccomp --net=br0 --ip=10.10.20.10 /etc/init...
      3868:root:bash -c /etc/init.d/rsyslog start; /etc/init.d/nginx st...
        3885:root:/usr/sbin/rsyslogd -c5 
        3912:root:nginx: master process /usr/sbin/nginx
          3913:www-data:nginx: worker process
          3914:www-data:nginx: worker process
          3916:www-data:nginx: worker process
          3917:www-data:nginx: worker process
        3915:root:sleep inf 
    #
    
  • Network stack: The sandbox uses a separate network stack, with different interfaces, its own routing table and firewall, and its own set of socket connections. The host firewall was set to forward TCP port 80 traffic to our sandbox, and to drop any connections originated on 10.10.20.0/24 network segment.
  • Seccomp: Seccomp (alias for “secure computing”) is a filtering mechanism that allows processes to specify an arbitrary filter of system calls (expressed as a Berkeley Packet Filter program) that should be forbidden. Berkeley Packet Filter support for seccomp was introduced in Linux kernel 3.5. It greatly reduces kernel attack surface. The feature is enabled using –seccomp option.
 

Step 5: Monitoring the web server

Check the server using the log files in /var/log directory inside the sandbox. To reach them you would have to join the sandbox using firejail –join command:

# firejail --join=3868
[root@debian ~]$

Specify the PID number for one of the processes running in sandbox as –join argument. The option will only work on Linux kernels 3.8 or newer, and it is equivalent to a terminal login. If you are using an earlier kernel, add an ssh server to your sandbox:

# firejail --private --seccomp --net=br0 --ip=10.10.20.10 \
"/etc/init.d/rsyslog start; \
/etc/init.d/ssh start; \
/etc/init.d/nginx start; \
sleep inf" &

Conclusion

I start with a mainstream web server (nginx) running on one of the most popular web server platforms (Debian). This provides a secure baseline for my setup. I sandbox the server using Linux namespaces feature in Linux kernel, thus increasing the security of the setup.

This is a comparison of a regular server setup and a restricted server setup in a security sandbox:

 
Regular setup Security sandbox setup
Filesystem Read-write Configurable, mostly read-only
Process table Access to all running processes Access only to processes running in the sandbox
Network Full local network access Controlled local network access
 

The same solid server security practices are required for both the regular and the sandbox case. An attacker gaining unauthorized root access is bad in both cases.

Related Posts

 

13 thoughts on “Securing a Web Server Using a Linux Namespaces Sandbox

  1. Pingback: Securing a Web Server Using a Linux Namespaces Sandbox | Hallow Demon

  2. Pingback: Links 18/6/2014: Red Hat to acquire eNovance | Techrights

  3. Domenico

    I’m using firejail version 0.9.30
    If I start firejail with a non root users and start a process listening on a port (like nc -l) inside the sandbox, i can’t connect to the port of the process from outside of the sandbox. If I start firejail as root it’s working as expected.
    Is this an expected behaviour ?
    Thanks

    Reply
    1. netblue30 Post author

      Seems to be working fine. As a regular user, you would need to listen (-l) on port numbers greater than 1024. As root you can below 1024. Example:

      $ firejail 
      Reading profile /etc/firejail/generic.profile
      Reading profile /etc/firejail/disable-mgmt.inc
      Reading profile /etc/firejail/disable-secret.inc
      Reading profile /etc/firejail/disable-common.inc
      Reading profile /etc/firejail/disable-history.inc
      
      ** Note: you can use --noprofile to disable generic.profile **
      
      Parent pid 18041, child pid 18042
      Child process initialized
      $ nc -l -p 77
      Can't grab 0.0.0.0:77 with bind : Permission denied
      $ nc -l -p 12345
      ^C
      
      Reply
  4. Domenico

    Thanks for the quick reply.

    I’m sorry for my bad english, more information on my project : I want to expose on Internet a service running on my raspberry.
    I have a firewall with two VLANs : 1 (internal LAN) and 9 (DMZ to which the sandbox is connected and port forwarded to internet by the firewall)
    This is the network configuration of the raspberry (Ip forwarding is enabled and i have a bridge br0 on VLAN 9) :

    root@raspberrypi:/# ifconfig
    eth0      Link encap:Ethernet  HWaddr b8:27:eb:54:54:94
              UP BROADCAST MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
    
    eth0.1    Link encap:Ethernet  HWaddr b8:27:eb:54:54:94
              inet addr:172.25.2.101  Bcast:172.25.2.255  Mask:255.255.255.0
              UP BROADCAST MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
    
    eth0.9    Link encap:Ethernet  HWaddr b8:27:eb:54:54:94
              UP BROADCAST MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
    
    lo        Link encap:Local Loopback
              inet addr:127.0.0.1  Mask:255.0.0.0
              UP LOOPBACK RUNNING  MTU:65536  Metric:1
              RX packets:876 errors:0 dropped:0 overruns:0 frame:0
              TX packets:876 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:79172 (77.3 KiB)  TX bytes:79172 (77.3 KiB)
    
    br0    Link encap:Ethernet  HWaddr b8:27:eb:54:54:94
              inet addr:172.25.1.101  Bcast:172.25.1.255  Mask:255.255.255.0
              UP BROADCAST MULTICAST  MTU:1500  Metric:1
              RX packets:71 errors:0 dropped:0 overruns:0 frame:0
              TX packets:40 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:6024 (5.8 KiB)  TX bytes:5055 (4.9 KiB)
    

    In a raspberry shell i run firejail :

    root@raspberrypi:/# firejail --name=jdownloader --net=br0 --ip=172.25.1.110  --dns=172.25.1.101
    Reading profile /etc/firejail/server.profile
    Reading profile /etc/firejail/disable-mgmt.inc
    
    ** Note: you can use --noprofile to disable server.profile **
    
    Parent pid 11731, child pid 11735
    The new log directory is /proc/11735/root/var/log
    
    Interface        MAC                IP               Mask             Status
    lo                                  127.0.0.1        255.0.0.0        UP
    eth0             c6:d0:a1:0f:07:3c  172.25.1.110     255.255.255.0    UP
    Default gateway 172.25.1.101
    DNS server 172.25.1.101
    
    Child process initialized
    [root@jdownloader /]$ nc -l 172.25.1.110 10000
    

    In another raspberry shell (outside sandbox)

    root@raspberrypi:~# telnet 172.25.1.110 9999
    Trying 172.25.1.110...
    telnet: Unable to connect to remote host: Connection refused
    root@raspberrypi:~# telnet 172.25.1.110 10000
    Trying 172.25.1.110...
    Connected to 172.25.1.110.
    Escape character is '^]'.
    dfdfdf
    dfdf
    

    In sandbox I see :

    [root@jdownloader /]$ nc -l 172.25.1.110 10000
    dfdfdf
    dfdf
    

    OK, it works

    But if i run firejail from a not root user, the sandbox an nc starts with the same output, but in the other shell i have :

    root@raspberrypi:~# telnet 172.25.1.110 10000
    Trying 172.25.1.110...
    
    telnet: Unable to connect to remote host: Connection timed out
    
    Reply
  5. Domenico

    I have news.

    If I start firejail with a not root user using the –noprofile option, then access from outside works like with root user.
    What in generic.profile can cause this behaviour only for non root users ?

    Reply
    1. netblue30 Post author

      I think it has something to do with the way you start the server using nc. If you start it as “nc -l 172.25.1.110 10000” I can see it opening a server on port 47601. If I replace this command with “nc -p 10000” the server is opened on 100000 and immediately it starts working.

      To verify what port nc is listening, I joined the sandbox from another terminal and run netstat:

      $ sudo firejail --join=jdownloader
      Switching to pid 27253, the first child process inside the sandbox
      $ netstat -ln
      Active Internet connections (only servers)
      Proto Recv-Q Send-Q Local Address           Foreign Address         State      
      tcp        0      0 0.0.0.0:47601           0.0.0.0:*               LISTEN     
      Active UNIX domain sockets (only servers)
      Proto RefCnt Flags       Type       State         I-Node   Path
      

      It should say like this:

      $ netstat -ln
      Active Internet connections (only servers)
      Proto Recv-Q Send-Q Local Address           Foreign Address         State      
      tcp        0      0 0.0.0.0:10000           0.0.0.0:*               LISTEN     
      Active UNIX domain sockets (only servers)
      Proto RefCnt Flags       Type       State         I-Node   Path
      
      Reply
      1. Domenico

        I can’t reproduce the strange behaviour you have when launch “nc -l 172.25.1.110 10000”.
        In my sandbox nc listen always on the same port, the only difference is the address, 0.0.0.0 if i use -p parameter.

        I have used nc as an example of a simple application listening on a port, but i have the same problem (a listening port can’t be contacted from outside of a sandbox launched with non root user) with other applications.

        As i said previously, if I use the –noprofile parameter the listening port can be contacted from outside of the sandbox when i start a sandbox from a non root user, so I think we have to find out what in the generic.profile causes this behaviour.

      2. netblue30 Post author

        The profile file is in /etc/firejail/generic.profile. You can go line by line and comment them out (add a # at the beginning of the line). My guess would be the line with “caps.drop all”.

  6. Domenico

    It’s not caps related, but it depends from the “netfilter” line.
    In man documentation –netfilter applies the following rules :

    *filter
    :INPUT DROP [0:0]
    :FORWARD DROP [0:0]
    :OUTPUT ACCEPT [0:0]
    -A INPUT -i lo -j ACCEPT
    -A INPUT -m state –state RELATED,ESTABLISHED -j ACCEPT
    -A INPUT -p icmp –icmp-type destination-unreachable -j ACCEPT
    -A INPUT -p icmp –icmp-type time-exceeded -j ACCEPT
    -A INPUT -p icmp –icmp-type echo-request -j ACCEPT
    COMMIT

    The line “:INPUT DROP [0:0]” clearly blocks every incoming connection.
    Launching firejail with a modified generic profile having the netfilter line changed in “netfilter=filename” and filename containing all the previous rules except the “:INPUT DROP [0:0]”, the connection to nc works as expected.

    Now the open question is : Why the netfilter options has not effect when firejail is launched as root ?

    Reply
      1. Domenico

        But if i start firejail as root with –debug, it’s shown “Installing network filter:” with the same rules as when it’s started as a normal user. So it’s a bug or what else ?

  7. Domenico

    I have done more tests :
    If firejail starts with plain –netfilter without filename, if starts as root no filter rules are applied, if starts as normal user the default filter rules are applied.
    If firejail starts with –netfilter=filename, filter rules are applied
    independently which user started it.

    I think it shoud be more explained in documentation the different behaviour when started as root or not.

    Reply

Leave a comment