Blocking Ads with Pi-hole in Kubernetes
May 25th 2020, 2:36pm
5 min read
One of the common things people do with a vanilla Raspberry Pi is to use it exclusively to
host Pi-Hole. If you haven’t heard of Pi-Hole, it’s essentially a
service that blocks advertisements via DNS. Imagine a /etc/hosts file with a gigantic
list of domains belonging to ad networks and forcing them to point to 0.0.0.0.
The simpler alternative to this approach is using browser extensions like Adblock Plus. To cover your entire network however, you would have to install an extension on every browser on every device, consuming memory and having to manage individual block lists separately. Mobile devices and apps also add to the decentralisation.
Since I already have an operational Kubernetes cluster, I wanted to see how different it would be managing Pi-Hole as a microservice rather than installed directly on a Pi.
    
    
Deploying to Kubernetes
Pi-hole already has images published on their Docker
Hub so the deployment manifest is simple,
requiring two areas of persistent storage for configuration data and the admin password to be configured in the environment variable WEBPASSWORD.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pihole-data
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pihole-dnsmasq
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pihole
  labels:
    app: pihole
spec:
  selector:
    matchLabels:
      app: pihole
  template:
    metadata:
      labels:
        app: pihole
    spec:
      containers:
      - name: pihole
        env:
        - name: WEBPASSWORD
          value: a-strong-password-here
        image: pihole/pihole:latest
        volumeMounts:
        - mountPath: /etc/pihole
          name: pihole-data
        - mountPath: /etc/dnsmasq.d
          name: pihole-dnsmasq
      volumes:
      - name: pihole-data
        persistentVolumeClaim:
          claimName: pihole-data
      - name: pihole-dnsmasq
        persistentVolumeClaim:
          claimName: pihole-dnsmasqWe’ll then need to expose the admin console on port 80 and the DNS service on port 53, but
this needs to be done on both TCP and UDP. Kubernetes does not allow you to create a
single service that handles both TCP and UDP, so we will have to create 2 services, then
force our load balancer to host them on the same external IP. I’m using Metal LB, which
supports this via the addition of
an allow-shared-ip annotation to both services and supplying a common identifier, which
I will list as pihole-svc.
apiVersion: v1
kind: Service
metadata:
  name: pihole-tcp
  annotations:
    metallb.universe.tf/allow-shared-ip: pihole-svc
spec:
  type: LoadBalancer
  selector:
    app: pihole
  externalTrafficPolicy: Local
  ports:
  - name: pihole-admin
    port: 80
    targetPort: 80
    protocol: TCP
  - name: dns-tcp
    port: 53
    targetPort: 53
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: pihole-udp
  annotations:
    metallb.universe.tf/allow-shared-ip: pihole-svc
spec:
  type: LoadBalancer
  selector:
    app: pihole
  externalTrafficPolicy: Local
  ports:
  - name: dns-udp
    port: 53
    targetPort: 53
    protocol: UDPOnce you kubectl create -f the manifests, you should see the single pod start up and the
load balancer assign a shared IP for the services.
NAME        TYPE          CLUSTER-IP    EXTERNAL-IP  PORT(S)                   AGE
pihole-tcp  LoadBalancer  10.121.27.29  10.0.0.77   80:30600/TCP,53:31765/TCP  1m
pihole-udp  LoadBalancer  10.103.12.15  10.0.0.77   53:32637/UDP               1mAdministration
You can now visit the admin console at http://10.0.0.77 (the IP assigned above) and login
using the password defined in the deployment. Pi-hole is actually usable right away
but you can choose to override some settings like upstream DNS in Settings > DNS or add
more block lists to the default in Group Management > Adlists. You can Google for
Pi-Hole lists where you will find many, many different lists you can add directly to
your Pi-Hole. Once you add new lists, you need to go to Tools > Update Gravity to
download the contents from those lists into your local database. Pi-hole will periodically
do this for you to refresh any changes in those lists.
Testing and Go-Live
To test if Pi-Hole is working, just perform a nslookup google.com 10.0.0.77 and check
that it resolves and appears in your admin console’s Query Log page. Once you’re
satisfied, go to your DHCP server (usually in your home router) and override the DNS
server to 10.0.0.77 (whatever your LB assigned). All new devices obtaining new DHCP
leases will henceforth use Pi-Hole to resolve hostnames and block matching domains (and
associated ads). You can then get some interesting metrics back in the admin console after
using it for a day or so.
    
    
Adding DHCP
Now that Pi-Hole has relieved your home router’s duties in resolving DNS, what if you also
wanted to relieve it of IP assignment duties? You’ll be happy to know you can centralise
this as well in Pi-Hole and it comes out-of-the-box. Unfortunately, since we’re in the
Kubernetes world, some extra work is involved. This “breaks” the container model a little
and adds some security considerations, so exercise your discretion. The core problem here
is that if Pi-Hole sits in a container, it cannot listen to DHCP requests on Layer 2
happening outside the cluster’s internal network. Hence, it will need access to the host
network as well as root access on the host to listen on ports 53 and 67. The actual
work involved is not too difficult: just add hostNetwork and securityContext to your
deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pihole
  labels:
    app: pihole
spec:
  selector:
    matchLabels:
      app: pihole
  template:
    metadata:
      labels:
        app: pihole
    spec:
      containers:
      - name: pihole
        securityContext:            privileged: true        env:
        - name: WEBPASSWORD
          value: a-strong-password-here
        image: pihole/pihole:latest
        volumeMounts:
        - mountPath: /etc/pihole
          name: pihole-data
        - mountPath: /etc/dnsmasq.d
          name: pihole-dnsmasq
      hostNetwork: true      volumes:
      - name: pihole-data
        persistentVolumeClaim:
          claimName: pihole-data
      - name: pihole-dnsmasq
        persistentVolumeClaim:
          claimName: pihole-dnsmasqYou’ll then need to edit your UDP service to expose the DHCP port.
apiVersion: v1
kind: Service
metadata:
  name: pihole-udp
  annotations:
    metallb.universe.tf/allow-shared-ip: pihole-svc
spec:
  type: LoadBalancer
  selector:
    app: pihole
  externalTrafficPolicy: Local
  ports:
  - name: dhcp-udp    port: 67    targetPort: 67    protocol: UDP  - name: dns-udp
    port: 53
    targetPort: 53
    protocol: UDPYou can then head into the admin console under Settings > DHCP and enable the DHCP
server, setting up your desired range and any static leases. Once this is enabled,
remember to disable your router’s DHCP server. Now you have a single control plane to
manage all local and remote hostname resolution. Happy ad-free life!