ariadne.space/content/blog/efficient-service-isolation...

84 lines
6.5 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
title: "Efficient service isolation on Alpine with VRFs"
date: "2021-09-13"
---
Over the weekend, a reader of my blog contacted me basically asking about firewalls.  Firewalls themselves are boring in my opinion, so lets talk about something Alpine can do that, as far as I know, no other distribution can easily do out of the box yet: service isolation using the base networking stack itself instead of netfilter.
## A note on netfilter
Linux comes with a powerful network filtering framework, called [netfilter](https://www.netfilter.org). In most cases, netfilter is performant enough to be used for packet filtering, and the newer `nftables` project provides a mostly user friendly interface to the system, by comparison to the older `iptables` and ip6tables commands.
However, when trying to isolate individual services, a firewall is sometimes more complicated to use, especially when it concerns services which connect outbound. Netfilter does provide hooks that allow for matching via `cgroup` or `PID`, but these are complicated to use and carry a significant performance penalty.
## seccomp and network namespaces
Two other frequently cited options are to use seccomp and network namespaces for this task. Seccomp is a natural solution to consider, but again has significant overhead, since all syscalls must be audited by the attached seccomp handler. Although seccomp handlers are eBPF programs and may be JIT compiled, the performance cost isnt zero.
Similarly, one may find network namespaces to be a solution here. And indeed, network namespaces are a very powerful tool. But because of the flexibility afforded, network namespaces also require a lot of effort to set up. Importantly though, network namespaces allow for the use of an alternate routing table, one that can be restricted to say, the management LAN.
## Introducing VRFs
Any network engineer with experience will surely be aware of VRFs. The VRF name is an acronym which stands for virtual routing and forwarding. On a router, these are interfaces that, when packets are forwarded to them, use an alternative routing table for finding the next destination. In that way, they are similar to Linuxs network namespaces, but are a lot simpler.
Thanks to the work of Cumulus Networks, Linux [gained support for VRF interfaces in Linux 4.3](https://lwn.net/Articles/632522/). And since Alpine 3.13, we have supported managing VRFs and binding services to them, primarily for the purpose of low-cost service isolation. Lets look at an example.
## Setting **up** the VRF
On our example server, we will have a management LAN of `10.20.30.0/24`. A gateway will exist at `10.20.30.1` as expected. The server itself will have an IP of `10.20.30.40`. We will a single VRF, in conjunction with the systems default route table.
### Installing the needed tools
By default, Alpine comes with Busyboxs iproute2 implementation. While good for basic networking use cases, it is recommended to install the real iproute2 for production servers. To use VRFs, you will need to install the real iproute2, using `apk add iproute2-minimal`, which will cause the corresponding ifupdown-ng modules to be installed as well.
### Configuring `/etc/network/interfaces`
We will assume the servers ethernet port is the venerable `eth0` interface in Alpine. First, we will want to set up the interface itself and its default route. If youve used the Alpine installer, this part should already be done, but we will include the configuration snippet for those following along.
auto eth0
iface eth0
address 10.20.30.40/24
gateway 10.20.30.1
The next step is to configure a VRF. In this case, we want to limit the network to just the management LAN, `10.20.30.0/24`. At the moment, ifupdown-ng does not support configuring interface-specific routes out of the box, but its coming in the next version. Accordingly, we will use iproute2 directly with a `post-up` directive.
auto vrf-management
iface vrf-management
requires eth0
vrf-table 1
pre-up ip -4 rule add pref 32765 table local
pre-up ip -6 rule add pref 32765 table local
pre-up ip -4 rule del pref 0
pre-up ip -6 rule del pref 0
pre-up ip -4 rule add pref 2000 l3mdev unreachable
post-up ip route add 10.20.30.0/24 dev eth0 table 1
This does four things: first it creates the management VRF, `vrf-management` using the second kernel route table (each network namespace may have up to 4,096 routing tables).  It also asserts that the `eth0` interface must be present and configured before the VRF is configured.  Next, it removes the default route lookup rules and moves them so that the VRFs will be checked first.  Finally, it then adds a route defining that the management LAN can be accessed through `eth0`. This allows egress packets to make their way back to clients on the management LAN.
In future versions of ifupdown-ng, the routing rule setup will be handled automatically.
### Verifying the VRF works as expected
Once a VRF is configured, you can use the ip vrf exec command to run a program in the specified VRF context. In our case, the management VRF lacks a default route, so we should be able to observe a failure trying to ping hosts outside the management VRF, using `ip vrf exec vrf-management ping 8.8.8.8` for example:
localhost:~# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=121 time=1.287 ms
^C
\--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.287/1.287/1.287 ms
localhost:~# ip vrf exec vrf-management ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: sendto: Network unreachable
Success!  Now we know that using `ip vrf exec`, we can launch services with an alternate routing table.
## Integration with OpenRC
Alpine presently uses OpenRC as its service manager, [although we plan to switch to s6-rc in the future](https://ariadne.space/2021/03/25/lets-build-a-new-service-manager-for-alpine/).  Our branch of OpenRC has support for using VRFs, for the declarative units.  In any of those files, you just add `vrf=vrf-management`, to the appropriate `/etc/conf.d` file, for example `/etc/conf.d/sshd` for the SSH daemon.
For services which have not been converted to use the declarative format, you will need to patch them to use `ip vrf exec` by hand.  In most cases, all you should need to do is use `${RC_VRF_EXEC}` in the appropriate place.
As for performance, this is _much_ more efficient than depending on netfilter, although the setup process is not as clean as I'd like it to be.