Traditional Reverse Proxy with Traefik

So the other day I needed to setup a reverse proxy server for a client of mine who wanted some internal servers accessible from the internet. I tested this with nginx but had issues with LetsEncrypt as well as some pages timing out.

I finally ended up with Traefik but boy did it take along time to get it configured as I was not familiar with Docker and most guides are Docker with other Docker Containers.

Following the guide below will get you up and running in no time with

  • Integrated LetsEncrypt SSL certificates, which will auto generate and renew.
  • Standalone systemd service for ease of use/management
  • A hardened/restricted Traefik service for added security.

This guide assumes the following has already been done:

  • Latest Debian/Ubuntu server has been installed/setup.
  • No other services are using ports 80/443
  • If server is behind NAT, ports 80 and 443 has been forwarded correctly.
  • DNS records have been correctly updated to point to Traefic Server.

Let’s start with setting up the working directories and files required to run Traefik and grabbing the Traefik binary.

mkdir /etc/traefik
touch /etc/traefik/traefik.toml
touch /etc/traefik/acme.json
chmod 0600 /etc/traefik/acme.json
wget https://github.com/containous/traefik/releases/download/v1.7.9/traefik
chmod +x traefik
mv traefik /usr/local/bin/

Next, we need to edit traefik.toml which holds configuration for Traefik. In the following sample configuration, it is configured to

  • Listen on HTTP and HTTPS (80/443)
  • Redirect HTTP to HTTPS
  • Enabled LetsEncrypt integration
  • Created 2x backend services (Plex and ERPNext)
  • Mapped frontends (URL you would enter on a browser) to backends
nano /etc/traefik/traefik.toml
debug = false

#Uncomment below if you selfsigned backends
#insecureSkipVerify = true

logLevel = "ERROR"
defaultEntryPoints = ["https","http"]

[entryPoints]
	[entryPoints.http]
	address = ":80"
		[entryPoints.http.redirect]
		entryPoint = "https"
	[entryPoints.https]
	address = ":443"
	[entryPoints.https.tls]

[retry]

[api]

[acme]
	email = "you@email.com"
	storage = "/etc/traefik/acme.json"
	entryPoint = "https"
	onHostRule = true

[acme.httpChallenge]
	entryPoint = "http"
	
[file]

[backends]
	[backends.plex]
		[backends.plex.servers.server1]
			url = "https://172.16.1.2"
	[backends.erpnext]
		[backends.erpnext.servers.server1]
			url = "http://172.16.1.1"

[frontends]
		[frontends.plex]
			backend = "plex"
			passHostHeader = true
		[frontends.plex.routes.test_1]
			rule = "Host:media.domain.com"
		[frontends.erpnext]
			backend = "erpnext"
			passHostHeader = true
		[frontends.erpnext.routes.test_1]
			rule = "Host:erp.domain.com"

Next we need to setup the systemd service. The following will setup the restricted systemd file

nano /etc/systemd/system/traefik.service
[Unit]
Description=Traefik
Documentation=https://docs.traefik.io
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/traefik
AssertPathExists=/etc/traefik

[Service]
Type=notify
ExecStart=/usr/local/bin/traefik -c /etc/traefik/traefik.toml
Restart=always
WatchdogSec=1s
ProtectSystem=strict
ReadWritePaths=/etc/traefik/acme.json
ReadOnlyPaths=/etc/traefik/traefik.toml
PrivateTmp=true
ProtectHome=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
LimitNPROC=1

[Install]
WantedBy=multi-user.target

Finally, reload the systemd service, enable the traefik service to auto start and start the actual service.

systemctl daemon-reload
systemctl enable traefik.service
systemctl start traefik.service

Sources:

https://blog.ssdnodes.com/blog/traefik-multiple-ssl-websites/
https://blog.cubieserver.de/2018/locking-down-traefik-systemd-service/

PowerDNS Recursor – Hiding/Private Zones via LUA

When deploying PowerDNS Recursor for our customers, we had an issue with how to hide some responses/zones to external IPs. The way we implemented this solution is via LUA scripting.

-- Set Private Zone
myDomain = newDN("private.com")

-- Whitelist IPs
myNetblock = newNMG()
myNetblock:addMask("192.168.0.0/24")
myNetblock:addMask("192.168.9.0/24")

function preresolve(dq)
	if dq.qname:isPartOf(myDomain) and myNetblock:match(dq.remoteaddr) and dq.qtype == pdns.A then
		-- if the dns requests is for the private zone by trusted IP, proceed to respond.
		return false
	elseif dq.qname:isPartOf(myDomain) and dq.qtype == pdns.A then
		-- if the dns requests is for the private zone by UNtrusted IP, do not respond for A record requests
		pdnslog ("Remote IP " .. dq.remoteaddr:toString() .. " for domain=" .. dq.qname:toString() .. ", type=" .. tostring(dq.qtype) )
		return true
	else
		-- function normally for everything else.
		return false
	end
end

In the above example, we set the zone/domain that needs to be private, next select which IP addresses are allowed to see the records and finally the logic via the preresolve() function.