Airfield @ Altitude: Building A Network Gateway With Alpine Linux

privb0x23
privb0x23
Jul 27, 2017 · 13 min read

Tl;dr

A simple network gateway can be created, using Alpine Linux and Policy Routing, that ensures that all traffic passes through a VPN with no leaks, that DNS lookups are performed with DNSCrypt and DNSSEC, and the internal network is protected from several types of network threat. Though the process requires significant investment of blood and treasure, the result enhances a small home network by providing more security and privacy. However, there is significant room for improvement, especially in ease of use and maintenance.

Introduction

An Analogy

Life in an isolated mountain village can be simple, which is fine. However, one is likely to want to venture out further afield to get the most from the wider world. This is potentially hazardous; one is likely to encounter surveillance, interference and throttling. A possible solution that reduces the risk is to build an alpine airfield and fly to a distant airport before going onwards to a final destination, thus avoiding some of the dangers. This airfield is for use by all of the community — no exceptions — and must prevent outsiders from intruding and causing harm within the village.

Requirements

A simple, small, secure and stable network gateway (the ‘airfield’) is required, such as might be used in a home (the ‘mountain village’), to access the Internets. It must guarantee that all traffic passes through a VPN connection with no unintended leaks, it must provide an internal DNS server (using an external DNSCrypt server that claims not to log requests and supports DNSSEC), and it must protect the internal end points from external network threats.

The separate required functions are:

  • Router.
  • NAT gateway to the Internets.
  • Packet filter (firewall).
  • VPN client gateway.
  • DNS caching server.
  • Use an upstream DNS server with DNSCrypt and DNSSEC.
  • NTP server.

The vision is for the gateway to sit behind a conventional — often ISP supplied — home router or modem, but in front of any network security monitoring and more functional internal network routers/appliances (e.g. OPNSense). Optionally, DNS blacklisting can reduce annoyance, privacy invasion, malware, and wasted traffic, by trying to block advertising, malvertising, known malware communication and unwanted web sites.

╭┈┈┈╮   ╭┈┈┈╮   ╭┈┈┈┈┈╮   ╭┈┈┈┈┈┈╮   ╭┈┈┈┈┈┈╮
┊ VPN ┊---┊ ISP ┊---┊ Router ┊---┊ Gateway ┊--┊ Internal ┊
╰┈┈┈╯ ╰┈┈┈╯ ╰┈┈┈┈┈╯ ╰┈┈┈┈┈┈╯ ╰┈┈┈┈┈┈╯

Assumptions

  • The ISP can record traffic metadata and inspect all traffic for advertising and/or surveillance.
  • The ISP might hijack DNS for advertising and/or censorship.
  • The consumer router/modem in use cannot be properly secured — due to general embedded/IoT insecurities and/or firmware updates not being available.
  • The external VPN server or service provider claims not to log, censor or modify traffic priority.
  • No internal services require external access or DNAT/port forwarding.
  • The gateway has one externally facing network interface and one internally facing network interface.

Threat Model

Assets need protection from Threats, so these must be explored to properly understand the scenario.

Assets

  • External DNS queries/responses.
  • Other traffic to/from the Internets.
  • Internal end points.
  • Gateway/airfield system itself.

Threats

  • ISP surveillance (DNS/unencrypted traffic, TLS/SSL certificates, SNI).
  • ISP modification or censorship.
  • ISP throttling or QoS (non net neutral behaviour).
  • External network based threat agents (malware/hackers).
  • Advertising, malvertising, and web client tracking.

Using Alpine Linux, an OpenVPN client, an Unbound DNS server, DNSCrypt-Proxy, nftables and IP routing rules, the first important steps are taken to protecting the security and privacy of a simple network’s assets against the above threats.


Alpine Base

A lightweight distribution, Alpine Linux uses a hardened kernel and toolchain, no systemd, LibreSSL, and musl libc. Though not perfect, it does provide a good base system to conduct network functions. The next steps are DNS, IP routing rules, VPN client, firewall, NTPd, and the optional DNS black hole.

Installation is beyond the current scope, so the remaining content starts from a v3.6.2 system (which is current at the time of writing). It is assumed that the basic networking setup has been performed so that Internet connectivity and DNS lookup works. Also, it’s often recommended to use sudo rather than a root login.

Ensure (for privacy reasons) that Alpine’s package manager (apk) uses an HTTPS repository.

/etc/apk/repositories

https://mirror.leaseweb.com/alpine/v3.6/main
@edge https://mirror.leaseweb.com/alpine/edge/main
@testing https://mirror.leaseweb.com/alpine/edge/testing
@community https://mirror.leaseweb.com/alpine/edge/community

Update and upgrade the base system.

# apk -v update
# apk -vi upgrade

DNS

Install the required packages for dnscrypt-proxy and unbound.

# apk -vi add dnscrypt-proxy@community unbound@edge

DNSCrypt-Proxy

Set up the CSV list of resolvers, and filter to include only the non-logging servers that support DNSSEC. Further editing can ensure that only those of a geographically appropriate location are used.

# mkdir -v /etc/dnscrypt-proxy
# awk -F ',' 'BEGIN {OFS=","} { if (tolower($8) == "yes") { if (tolower($9) == "yes") print } }' "/usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv" > "/etc/dnscrypt-proxy/dnscrypt-resolvers.csv"

Configure the daemon options, potentially using IPv6 if desired, with a randomly selected resolver from the CSV list.

/etc/conf.d/dnscrypt-proxy

DNSCRYPT_LOCALIP="[::1]:54"
DNSCRYPT_ARGS="--tcp-only --resolvers-list=/etc/dnscrypt-proxy/dnscrypt-resolvers.csv -R random"

Modify the default openrc script to use the above configuration.

/etc/init.d/dnscrypt-proxy

--local-address=${DNSCRYPT_LOCALIP:-127.0.0.1:53}
${DNSCRYPT_ARGS}"

Start the daemon and enable on boot, then test DNS lookup.

# rc-service dnscrypt-proxy start
# rc-update add dnscrypt-proxy default
# dig @::1 -p 54 google.com

The resolvers CSV list can be updated every so often.

Unbound

Unbound will cache DNS results for the internal network. Configure unbound to use the dnscrypt-proxy resolver, and limit the permitted client IP addresses if desired.

/etc/unbound/unbound.conf

server:
verbosity: 1
interface: ::1
interface: 127.0.0.1
interface: [insert.internal.interface.ip]
port: 53
outgoing-interface: ::1
cache-min-ttl: 3600
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
do-daemonize: yes
access-control: ::0/0 refuse
access-control: 0.0.0.0/0 refuse
access-control: ::1 allow
access-control: ::ffff:127.0.0.1 allow
access-control: 127.0.0.0/8 allow
access-control: [insert internal network] allow
use-syslog: yes
log-queries: no
root-hints: "/etc/unbound/root.hints"
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
qname-minimisation: no
use-caps-for-id: yes
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 100.64.0.0/10
private-address: 192.0.0.0/24
private-address: 192.0.2.0/24
private-address: 198.18.0.0/15
private-address: 192.51.100.0/24
private-address: 203.0.113.0/24
private-address: 240.0.0.0/4
private-address: 255.255.255.255/32
private-address: fd00::/8
private-address: fe80::/10
private-address: ::ffff:0:0/96
private-address: 2001:10::/28
private-address: 2001:db8::/32
do-not-query-localhost: no
prefetch: yes
prefetch-key: yes
auto-trust-anchor-file: "/etc/unbound/trusted-key.key"
val-clean-additional: yes
remote-control:
control-enable: no
forward-zone:
name: "."
forward-addr: ::1@54

It might be wise to fetch the latest copy of root.hints from InterNIC. Add the wget (or similar) package if it is not already installed. Copy in the DNSSEC root key file, set file and directory permissions, start the daemon and enable start at boot.

# wget 'https://www.internic.net/domain/named.cache' -O '/etc/unbound/root.hints'
# cp -v /usr/share/dnssec-root/trusted-key.key /etc/unbound/
# chown -v unbound:unbound /etc/unbound/trusted-key.key
# chown -v 0:unbound /etc/unbound
# chmod -v 770 /etc/unbound
# unbound-checkconf
# service unbound start
# rc-update add unbound default

Edit resolv.conf to use the local DNS server.

/etc/resolv.conf

nameserver ::1

Prevent the file from being changed.

# chattr +i /etc/resolv.conf

Check DNS lookup works.

# dig +dnssec dnscrypt.org
# nslookup sigfail.verteiltesysteme.net

IP Routing Rules

I n order to prevent any leaks outside the VPN, iproute2 can be used to create a set of rules for choosing a routing table to use — Policy Routing. These rules are used by Linux prior to routing, which makes them very useful. The rules apply to both forwarded packets and those originating from the gateway itself.

Based on a guide for hardening a client VPN connection, entirely separate routing tables can be used for normal traffic passing over the VPN tunnel and for the VPN data and control traffic.

The general premise is that the normal routing table (main) handles non VPN tunneled traffic — e.g. local networks, ISP based e-mail servers, and VPN servers. All other traffic — going via the VPN — is handled by a second routing table (23) that is created when the VPN is connected (see later for details).

The downside to this solution is that it requires an up-to-date list of IP addresses that do not go through the VPN so that the rules can direct traffic accordingly. A simple list of IPs in a text file can be used by a simple shell script to construct the rules, but the list needs to be generated somehow. The shell script can run on boot, and can be rerun when the IP list changes.

First, ensure a clean rule set is used as a base.

Second, add in any local networks (so the insecure modem/router web interface can be accessed) and ISP servers.

Thirdly, given a list of VPN server IP addresses, add the rules for each IP.

Finally, the rules for any other traffic to use the VPN routing table (23).

/etc/local.d/10-ip_routing_rules.start

#! /usr/bin/env ship rule flush
ip rule add lookup main table main pref 32766
ip rule add lookup default table main pref 32767
ip rule add to [local.network/netmask] table main pref 500
ip rule add to [isp.server.ip.address] table main pref 500
ips=$(cat "/etc/ips.vpn")set -- ${ips}
for ipaddr in ${ips}; do
ip rule add to "${ipaddr}" table main pref 1001
ip rule add to "${ipaddr}" unreachable pref 1002
done
ip rule add table 23 pref 1003
ip rule add unreachable pref 1004

/etc/ips.vpn

first.ip.address.here
second.ip.address.here
etc.etc.etc.etc

Execute the rules script and ensure it is run on startup.

# chmod -v 755 /etc/local.d/10-ip_routing_rules.start
# /etc/local.d/10-ip_routing_rules.start
# rc-update add local boot

The list of current rules can be viewed to confirm the changes. Check it matches with the main routing table.

# ip rule show
# ip route show

VPN Client

Which VPN provider to use, or whether to create your own, is beyond the current scope (there are some guides out there). Be mindful of their geographic and legal situation, and use only those that claim not to log, censor or shape traffic.

Install the required packages and ensure the tun kernel module is loaded.

# apk -vi add openvpn
# modprobe tun

/etc/modules

tun

Next, configure the VPN client, though some VPN servers require different options so tweaking is normally necessary. Authorisation may be via client certificate or username/passphrase. As DNS will not function prior to the tunnel’s creation, there is a choice between using VPN server IP addresses in the configuration file or placing the VPN server IPs in the /etc/hosts file (which will require periodic updates). Both options are not ideal, but it may be simpler to only have to update the hosts file when IP addresses change.

/etc/openvpn/vpn.conf

client
dev tun
# change to tcp if desired/required
proto udp
explicit-exit-notify 5
port [insert port number]
# add additional remote entries for more choice
remote [insert server]
remote-random
resolv-retry infinite
verb 0
mute 1
nobind
comp-lzo
user openvpn
group openvpn
ping 10
ping-restart 60
reneg-sec 3600
replay-window 128 30
hand-window 37
mssfix 1400
txqueuelen 686
cipher AES-256-CBC
auth SHA512
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA
tls-version-min 1.2
remote-cert-tls server
key-direction 1
key-method 2
persist-key
persist-tun
persist-remote-ip
auth-user-pass [insert auth file path]
<ca>
[insert cert]
</ca>
<tls-auth>
[insert key]
</tls-auth>
script-security 2
setenv OPENVPN_ROUTE_TABLE 23
route-noexec
route-up /usr/local/lib/openvpn/route.sh
route 0.0.0.0 0.0.0.0

Fetch the route creation script that’s required for the separate routing table.

# wget 'https://www.agwa.name/blog/post/hardening_openvpn_for_def_con/media/route' -O '/usr/local/lib/openvpn/route.sh'
# chmod -v 755 /usr/local/lib/openvpn/route.sh

Edit the hosts file to enable the VPN client to resolve the VPN servers’ domain names.

/etc/hosts

ip.address.of.server1    vpnserver1.external.domain.name
ip.address.of.server2 vpnserver2.external.domain.name

Create init.d symlink and enable on startup.

# cd /etc/init.d
# ln -s openvpn opevpn.vpn
# rc-update add openvpn.vpn default

Enable packet forwarding.

# sysctl -w net.ipv4.ip_forward=1

Permanently enable forwarding.

/etc/sysctl.d/01-router.conf

net.ipv4.ip_forward = 1

Start the VPN then restart DNS services.

# service openvpn.vpn start
# service dnscrypt-proxy restart
# service unbound start

Check the VPN works.

# ip address show
# ip route show table 23
# ping google.com

Firewall

IPTables is perhaps not as good as the newer NFTables, so this is a good opportunity to stay current. Add the required packages.

# apk -vi add nftables

The most important thing that nft needs to do is NAT. Ingress filtering is the next priority, then optionally egress filtering (care is needed to not break various applications). DNAT of DNS and NTP is another useful function. Below is a sample rule set, though it needs tweaking to work with particular situations.

/etc/nftables/filter

#! /usr/sbin/nft -f# interfaces
define if_ext = "eth0"
define if_int = "eth1"
define if_vpn = "tun0"
# ip addresses
define ip_int = [insert.internal.interface.ip]
define ip_router = [insert.router.modem.ip]
# ports
define pt_vpn = [insert vpn server port(s)]
define pt_ssh = 22
define pt_dns = 53
define pt_dnscrypt = 443
define pt_web = { 80, 443 }
define pt_ntp = 123
table inet filter { chain global {
# state
ct state invalid counter drop comment "drop invalid packets"
ct state { established, related } counter accept
}
# icmp
chain fl-icmp {
ip protocol icmp counter accept
ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } counter accept
}
chain fl-host-vpn {
udp dport $pt_vpn counter accept
}
chain fl-host-dns {
udp dport $pt_dns counter accept
tcp dport $pt_dns counter accept
tcp dport $pt_dnscrypt counter accept
}
chain fl-int-dns {
udp dport $pt_dns counter accept
tcp dport $pt_dns counter accept
}
chain fl-ntp {
udp dport $pt_ntp counter accept
}
chain fl-web {
tcp dport $pt_web counter accept
}
chain fl-other {
tcp dport $pt_ssh counter accept
}
chain input {
type filter hook input priority 0;
policy drop;
# loopback
iifname lo accept
jump global
ip protocol vmap { icmp : jump fl-icmp }
# from internal network to host
iifname $if_int ip daddr $ip_int jump fl-int-dns
iifname $if_int ip daddr $ip_int jump fl-ntp
# from internal network to modem/router
iifname $if_int ip daddr $ip_router jump fl-web
# from internal network to any
iifname $if_int jump fl-web
iifname $if_int jump fl-other
}
chain forward {
type filter hook forward priority 0;
policy drop;
jump global
ip protocol vmap { icmp : jump fl-icmp }
oifname $if_ext ip daddr $ip_router jump fl-web
oifname $if_vpn jump fl-web
oifname $if_vpn jump fl-other
}
chain output {
type filter hook output priority 0;
policy drop;
# loopback
oifname lo accept
jump global
ip protocol vmap { icmp : jump fl-icmp }
# to vpn servers
oifname $if_ext jump fl-host-vpn
# via vpn
oifname $if_vpn jump fl-host-dns
oifname $if_vpn jump fl-ntp
oifname $if_vpn jump fl-web
oifname $if_vpn jump fl-other
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
policy accept;
# dnat
iifname $if_int ip daddr != $ip_int udp dport $pt_dns dnat $ip_int
iifname $if_int ip daddr != $ip_int udp dport $pt_ntp dnat $ip_int
}
chain postrouting {
type nat hook postrouting priority 100;
policy accept;
# nat
oifname $if_ext masquerade
oifname $if_vpn masquerade
}
}

Modify the default init script behaviour to not save on stop.

/etc/conf.d/nftables

SAVE_ON_STOP="no"

Flush then enable the rules, view them to confirm, then ensure they are created on boot.

# nft flush ruleset
# chmod -v 700 /etc/nftables/filter
# /etc/nftables/filter
# nft list ruleset -a | less
# service nftables save
# rc-update add nftables boot

There seems to be a bug in the rule set saving of ICMPv6 which creates an invalid file, so this needs to be fixed. Also reset the counters.

# sed -i 's/icmpv6 type/ip6 nexthdr icmpv6 icmpv6 type/;s/packets [0-9]\+/packets 0/;s/bytes [0-9]\+/bytes 0/' "/var/lib/nftables/rules-save"

Some kernel configuration can be adjusted, as desired. These can be placed, as with IP forwarding earlier, in a file for permanence (e.g. /etc/sysctl.d/02-filter.conf).

# sysctl -w net.ipv4.ip_dynaddr=0
# sysctl -w net.ipv4.conf.all.accept_source_route=0
# sysctl -w net.ipv4.conf.all.accept_redirects=0
# sysctl -w net.ipv4.conf.all.log_martians=0
# sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
# sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1
# sysctl -w net.ipv4.tcp_syncookies=1
# sysctl -w net.ipv4.tcp_timestamps=1
# for i in /proc/sys/net/ipv4/conf/*/rp_filter ; do echo 2 > "${i}"; done

NTPd

Enable OpenNTPd and ensure it’s listening.

# setup-ntp

/etc/ntpd.conf

listen on *

Restart to enable the changes and check the listening ports.

# service openntpd restart
# netstat -anp | less

DNS Black Hole

Unbound can be configured to resolve undesirable domains to 0.0.0.1 as a simple way to try to prevent advertising, tracking and malware. There are several lists of domains that can be fetched and converted into unbound configuration format. Any internal requests for these domains will result in an unusable IP address and stop the internal application from communicating with the real external server.

Each unwanted domain can be added to a list of filters.

/etc/unbound/filter.conf

local-zone: "unwanted.external.domain.name" redirect
local-data: "unwanted.external.domain.name A 0.0.0.1"

Add the reference to the unbound configuration.

/etc/unbound/unbound.conf

server:
include: "/etc/unbound/filter.conf"

Check the file syntax and restart unbound.

# unbound-checkconf
# service unbound restart

Evaluation

The above steps take some considerable time to complete, including tweaking and adjustment. Anyone not familiar with Linux and networks will have a steep learning curve to create a working system. It’s not perfect, by any means.

Limitations

  • No failover or redundancy.
  • No DHCP server for end points.
  • No easy to use configuration or web interface.
  • Static routes must be managed manually.
  • Non-VPN IP addresses and the hosts file need to be updated periodically.
  • A lack of a built in software Wi-Fi Access Point.

However, it is possible to engineer a secure network gateway. It’s tough to overcome the security and privacy issues involved, with integrating multiple systems together complicating matters. Making a sufficiently secure VPN gateway with no leaks seems to be viable in Linux, but an easy to use web interface is nowhere to be seen.

Future Work

There’s much improvement to be made. Vulnerability assessment should be carried out to test the setup and check if the gateway is acting as expected.

  • Embed the gateway within a larger network — with a DHCP server, an internal Pi-Hole DNS server, a hostapd based Wi-Fi AP, an IDS, and a web proxy.
  • Enhance the IP routing rules and nftables rules, including rejecting bogons and make better use of policy routing.
  • Improve IPv6 functionality.
  • Use cron to automate the update of IP addresses and other resources.
  • Use a monitoring system to be aware of events and activity.
  • Use passive DNS for assisting with security incidents.
  • Isolate the individual services to minimise impact in the event of a breach.
  • Did someone say containers?
  • Create an OpenBSD based version.

Sources

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade