├── Caddyfile ├── LICENSE.md ├── README.md ├── caddy.Dockerfile ├── docker-compose.yml ├── hosts.example.yml └── playbook.yml /Caddyfile: -------------------------------------------------------------------------------- 1 | dns.example.com 2 | 3 | reverse_proxy 10.0.0.3:80 4 | 5 | tls you@example.com { 6 | # I use cloudflare here for DNS, but you can use any provider 7 | dns cloudflare {env.CLOUDFLARE_API_TOKEN} 8 | resolvers 10.0.0.3 9 | } 10 | 11 | # Not necessary, but built-in compression can speed things up a bit 12 | encode zstd gzip 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ben Balter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Docker Compose and Ansible configuration for running Pi-Hole, Cloudflared, and Caddy 2 | 3 | Example configuration for using Pi-Hole, Cloudflared, Docker Compose, Ansible, and Caddy to over-engineer your home network for privacy and security. 4 | 5 | ## Details 6 | 7 | See [How I re-over-engineered my home network for privacy and security](https://ben.balter.com/2021/09/01/how-i-re-over-engineered-my-home-network/) (and [How I over-engineered my home network for privacy and security](https://ben.balter.com/2020/12/04/over-engineered-home-network-for-privacy-and-security/)). 8 | 9 | ## Usage 10 | 11 | 1. Download the [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and flash the latest version of Raspberry Pi OS *Lite*. 12 | 2. Run `ansible-playbook playbook.yml --inventory hosts.yml` 13 | 3. Sit back and wait until you have a fully configured PiHole running in about 5-10 minutes 14 | -------------------------------------------------------------------------------- /caddy.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:builder AS builder 2 | 3 | RUN xcaddy build \ 4 | --with github.com/caddy-dns/cloudflare 5 | 6 | FROM caddy:latest 7 | 8 | COPY --from=builder /usr/bin/caddy /usr/bin/caddy 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | cloudflared: 5 | container_name: cloudflared 6 | restart: unless-stopped 7 | # Cloudflared doesn't have an armvf image, so we build from source 8 | build: https://github.com/cloudflare/cloudflared.git 9 | command: proxy-dns 10 | environment: 11 | # Replace with your Cloudflare Gateway domain or a public DNS over HTTPS server 12 | TUNNEL_DNS_UPSTREAM: "https://XXX.cloudflare-gateway.com/dns-query" 13 | TUNNEL_DNS_BOOTSTRAP: "https://1.1.1.2/dns-query" 14 | TUNNEL_DNS_ADDRESS: "0.0.0.0" 15 | TUNNEL_DNS_PORT: "53" 16 | 17 | # I'm pretty sure cloudflared doesn't use the bootstrap server, so we define it here too 18 | dns: 19 | - 1.1.1.2 20 | - 1.0.0.2 21 | networks: 22 | net: 23 | ipv4_address: 10.0.0.2 24 | healthcheck: 25 | test: ["CMD", "cloudflared", "version"] 26 | 27 | pihole: 28 | container_name: pihole 29 | restart: unless-stopped 30 | image: pihole/pihole 31 | secrets: 32 | - pihole_web_password 33 | environment: 34 | # Replace with your desired configuration 35 | TZ: America/New_York 36 | DNSSEC: "true" 37 | DNS_BOGUS_PRIV: "true" 38 | DNS_FQDN_REQUIRED: "true" 39 | TEMPERATUREUNIT: f 40 | PIHOLE_DNS_: "10.0.0.2" 41 | WEBPASSWORD_FILE: /run/secrets/pihole_web_password 42 | REV_SERVER: "true" 43 | REV_SERVER_TARGET: "192.168.1.1" 44 | REV_SERVER_CIDR: "192.168.0.0/16" 45 | VIRTUAL_HOST: dns.example.com 46 | ports: 47 | - "53:53/tcp" 48 | - "53:53/udp" 49 | volumes: 50 | - './etc-pihole/:/etc/pihole/' 51 | - './etc-dnsmasq.d/:/etc/dnsmasq.d/' 52 | networks: 53 | net: 54 | ipv4_address: 10.0.0.3 55 | dns: 56 | - "10.0.0.2" 57 | depends_on: 58 | - cloudflared 59 | healthcheck: 60 | test: ["CMD", "dig", "+norecurse", "+retry=0", "@127.0.0.1", "pi.hole"] 61 | caddy: 62 | build: 63 | context: . 64 | dockerfile: caddy.Dockerfile 65 | container_name: caddy 66 | restart: unless-stopped 67 | ports: 68 | - "80:80" # For HTTP -> HTTPS redirects 69 | - "443:443" 70 | volumes: 71 | - $PWD/Caddyfile:/etc/caddy/Caddyfile 72 | - caddy_data:/data 73 | - caddy_config:/config 74 | env_file: 75 | - .caddy.env 76 | dns: 77 | - 1.0.0.3 78 | healthcheck: 79 | test: ["CMD", "caddy", "version"] 80 | depends_on: 81 | - pihole 82 | - cloudflared 83 | networks: 84 | net: {} 85 | 86 | volumes: 87 | caddy_data: 88 | external: true 89 | caddy_config: 90 | 91 | networks: 92 | net: 93 | driver: bridge 94 | ipam: 95 | config: 96 | - subnet: 10.0.0.0/29 97 | 98 | # PiHole Web password lives in a .pihole_web_password to keep it out of the config 99 | secrets: 100 | pihole_web_password: 101 | file: .pihole_web_password 102 | -------------------------------------------------------------------------------- /hosts.example.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | 192.168.1.2: 4 | ansible_user: pi 5 | ansible_python_interpreter: auto 6 | -------------------------------------------------------------------------------- /playbook.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | # Allows you to SSH in to the PiHole via SSH, instead of password auth, pulling from your GitHub Public key 4 | - name: Ensure SSH Key is authorized 5 | authorized_key: 6 | user: pi 7 | state: present 8 | key: https://github.com/benbalter.keys 9 | 10 | # Ensure PiHole password is not the default 11 | # Here I'm using 1Password as my secret store, but you could use another source 12 | - name: Change pi user password 13 | become: true 14 | user: 15 | name: pi 16 | update_password: always 17 | password: "{{ lookup('community.general.onepassword', 'PiHole', field='Pi@ login') | password_hash('sha512') }}" 18 | 19 | # Update system-level dependencies 20 | - name: update and upgrade apt packages 21 | become: true 22 | apt: 23 | upgrade: dist 24 | update_cache: true 25 | 26 | # Set Static IP of PiHole so other devices can query it for DNS lookups 27 | - name: Install network manager 28 | become: true 29 | apt: 30 | name: network-manager 31 | state: present 32 | - name: configure network 33 | become: true 34 | community.general.nmcli: 35 | state: present 36 | conn_name: eth0 37 | ifname: eth0 38 | type: ethernet 39 | ip4: 192.168.1.2/24 40 | gw4: 192.168.1.1 41 | dns4: 42 | - 1.1.1.2 43 | 44 | # Ensure timestamps are in my local timezone 45 | - name: set timezone 46 | become: true 47 | community.general.timezone: 48 | name: America/New_York 49 | 50 | # A deploy key allows you to pull (or push) from a private GitHub repo 51 | - name: Ensure deploy key is present 52 | community.crypto.openssh_keypair: 53 | path: "~/.ssh/id_github" 54 | type: ed25519 55 | register: deploy_key 56 | 57 | # If a new deploy key is generated, authorize it for the repo 58 | # I'm using 1Password as my secret store, but you could use another source 59 | - name: Ensure deploy key is authorized 60 | community.general.github_deploy_key: 61 | key: "{{ deploy_key.public_key }}" 62 | name: Raspberry Pi 63 | state: present 64 | owner: benbalter 65 | repo: pi-hole 66 | token: "{{ lookup('community.general.onepassword', 'PiHole', field='GitHub Token') }}" 67 | 68 | - name: Install docker dependencies 69 | become: true 70 | apt: 71 | name: "{{ item }}" 72 | state: present 73 | update_cache: true 74 | loop: 75 | - apt-transport-https 76 | - ca-certificates 77 | - curl 78 | - gnupg 79 | - lsb-release 80 | - python3-pip 81 | - python3-setuptools 82 | - name: add Docker GPG key 83 | become: true 84 | apt_key: 85 | url: https://download.docker.com/linux/debian/gpg 86 | state: present 87 | - name: add docker repository to apt 88 | become: true 89 | apt_repository: 90 | repo: deb [arch=armhf signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian buster stable 91 | state: present 92 | - name: install docker 93 | become: true 94 | apt: 95 | name: "{{ item }}" 96 | state: present 97 | loop: 98 | - docker-ce 99 | - docker-ce-cli 100 | - containerd.io 101 | - name: Add user to docker group 102 | become: true 103 | user: 104 | name: pi 105 | groups: docker 106 | append: true 107 | - name: Enable & Start Docker service 108 | become: true 109 | service: 110 | name: docker 111 | enabled: true 112 | state: started 113 | - name: Install pip components 114 | pip: 115 | executable: pip3 116 | name: 117 | - docker 118 | - docker-compose 119 | - virtualenv 120 | 121 | # I version my config in a private Git Repo, so I clone it down using the deploy key 122 | # Note: This will not work without modification, as it's a private repo 123 | - name: Clone GitHub repo 124 | git: 125 | repo: git@github.com:benbalter/pi-hole.git 126 | dest: /home/pi/pi-hole/ 127 | clone: true 128 | update: true 129 | key_file: ~/.ssh/id_github 130 | accept_hostkey: true 131 | 132 | # Automatically upgrade apt packages 133 | - name: install unattended upgrades 134 | become: true 135 | apt: 136 | name: unattended-upgrades 137 | state: present 138 | - name: Setup unattended upgrades 139 | debconf: 140 | name: unattended-upgrades 141 | question: unattended-upgrades/enable_auto_updates 142 | vtype: boolean 143 | value: "true" 144 | 145 | # Prevents SSH brute force attacks 146 | - name: install fail2ban 147 | become: true 148 | apt: 149 | name: fail2ban 150 | state: present 151 | 152 | # Install and enable NTP to ensure the clock remains accurate 153 | - name: install ntp 154 | become: true 155 | apt: 156 | name: ntp 157 | state: present 158 | - name: enable ntp 159 | service: 160 | name: ntp 161 | state: started 162 | enabled: true 163 | 164 | # Installs firewall 165 | - name: install ufw 166 | become: true 167 | apt: 168 | name: ufw 169 | state: present 170 | 171 | # Rate limits SSH attempts 172 | - name: limit ssh 173 | become: true 174 | community.general.ufw: 175 | rule: limit 176 | port: ssh 177 | proto: tcp 178 | 179 | # Firewall rules 180 | - name: Allow all access to SSH, DNS, and WWW 181 | become: true 182 | community.general.ufw: 183 | rule: allow 184 | app: '{{ item }}' 185 | loop: 186 | - SSH 187 | - DNS 188 | - WWW 189 | - WWW Secure 190 | - name: enable ufw and default to deny 191 | become: true 192 | ufw: 193 | state: enabled 194 | default: deny 195 | 196 | # Set PiHole (Web Admin) password, referenced above. 197 | # I'm using 1Password, but you could use any secret store. 198 | - name: Set Pi-Hole secret 199 | copy: 200 | dest: /home/pi/pi-hole/.pihole_web_password 201 | content: "{{ lookup('community.general.onepassword', 'Raspberry pi', field='password') }}" 202 | 203 | - name: Set Caddy secret 204 | copy: 205 | dest: /home/pi/pi-hole/.caddy.env 206 | # I'm using 1Password here, but you could use any secret store you wanted 207 | content: "CLOUDFLARE_API_TOKEN={{ lookup('community.general.onepassword', 'Raspberry pi', field='Cloudflare Token') }}" 208 | mode: 0700 209 | 210 | - name: Create and start docker compose services 211 | community.docker.docker_compose: 212 | # Change to path to your docker-compose.yml. See below for how to clone a repo 213 | project_src: /home/pi/pi-hole 214 | pull: true 215 | build: true 216 | remove_orphans: true 217 | register: output 218 | 219 | --------------------------------------------------------------------------------