[
  {
    "path": "Caddyfile",
    "content": "dns.example.com\n\nreverse_proxy 10.0.0.3:80\n\ntls you@example.com {\n  # I use cloudflare here for DNS, but you can use any provider\n  dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n  resolvers 10.0.0.3\n}\n\n# Not necessary, but built-in compression can speed things up a bit\nencode zstd gzip\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2021 Ben Balter\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Example Docker Compose and Ansible configuration for running Pi-Hole, Cloudflared, and Caddy\n\nExample configuration for using Pi-Hole, Cloudflared, Docker Compose, Ansible, and Caddy to over-engineer your home network for privacy and security.\n\n## Details\n\nSee [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/)).\n\n## Usage\n\n1. Download the [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and flash the latest version of Raspberry Pi OS *Lite*.\n2. Run `ansible-playbook playbook.yml --inventory hosts.yml`\n3. Sit back and wait until you have a fully configured PiHole running in about 5-10 minutes\n"
  },
  {
    "path": "caddy.Dockerfile",
    "content": "FROM caddy:builder AS builder\n\nRUN xcaddy build \\\n    --with github.com/caddy-dns/cloudflare\n\nFROM caddy:latest\n\nCOPY --from=builder /usr/bin/caddy /usr/bin/caddy\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\n\nservices:\n  cloudflared:\n    container_name: cloudflared\n    restart: unless-stopped\n    # Cloudflared doesn't have an armvf image, so we build from source\n    build: https://github.com/cloudflare/cloudflared.git\n    command: proxy-dns\n    environment:\n      # Replace with your Cloudflare Gateway domain or a public DNS over HTTPS server\n      TUNNEL_DNS_UPSTREAM: \"https://XXX.cloudflare-gateway.com/dns-query\"\n      TUNNEL_DNS_BOOTSTRAP: \"https://1.1.1.2/dns-query\"\n      TUNNEL_DNS_ADDRESS: \"0.0.0.0\"\n      TUNNEL_DNS_PORT: \"53\"\n\n    # I'm pretty sure cloudflared doesn't use the bootstrap server, so we define it here too\n    dns:\n      - 1.1.1.2\n      - 1.0.0.2\n    networks:\n      net:\n        ipv4_address: 10.0.0.2\n    healthcheck:\n      test: [\"CMD\", \"cloudflared\", \"version\"]\n\n  pihole:\n    container_name: pihole\n    restart: unless-stopped\n    image: pihole/pihole\n    secrets:\n      - pihole_web_password\n    environment: \n      # Replace with your desired configuration\n      TZ: America/New_York\n      DNSSEC: \"true\"\n      DNS_BOGUS_PRIV: \"true\"\n      DNS_FQDN_REQUIRED: \"true\"\n      TEMPERATUREUNIT: f\n      PIHOLE_DNS_: \"10.0.0.2\"\n      WEBPASSWORD_FILE: /run/secrets/pihole_web_password\n      REV_SERVER: \"true\"\n      REV_SERVER_TARGET: \"192.168.1.1\"\n      REV_SERVER_CIDR: \"192.168.0.0/16\"\n      VIRTUAL_HOST: dns.example.com\n    ports: \n      - \"53:53/tcp\"\n      - \"53:53/udp\"\n    volumes:\n      - './etc-pihole/:/etc/pihole/'\n      - './etc-dnsmasq.d/:/etc/dnsmasq.d/'\n    networks:\n      net:\n        ipv4_address: 10.0.0.3\n    dns:\n      - \"10.0.0.2\"\n    depends_on:\n      - cloudflared\n    healthcheck:\n      test: [\"CMD\", \"dig\", \"+norecurse\", \"+retry=0\", \"@127.0.0.1\", \"pi.hole\"]\n  caddy:\n    build:\n      context: .\n      dockerfile: caddy.Dockerfile\n    container_name: caddy\n    restart: unless-stopped\n    ports:\n      - \"80:80\" # For HTTP -> HTTPS redirects\n      - \"443:443\"\n    volumes:\n      - $PWD/Caddyfile:/etc/caddy/Caddyfile\n      - caddy_data:/data\n      - caddy_config:/config\n    env_file:\n      - .caddy.env\n    dns:\n      - 1.0.0.3\n    healthcheck:\n        test: [\"CMD\", \"caddy\", \"version\"]\n    depends_on:\n      - pihole\n      - cloudflared\n    networks:\n      net: {}\n\nvolumes:\n  caddy_data:\n    external: true\n  caddy_config:\n\nnetworks:\n  net:\n    driver: bridge\n    ipam:\n     config:\n       - subnet: 10.0.0.0/29\n\n# PiHole Web password lives in a .pihole_web_password to keep it out of the config\nsecrets:\n  pihole_web_password: \n    file: .pihole_web_password\n"
  },
  {
    "path": "hosts.example.yml",
    "content": "all:\n  hosts:\n    192.168.1.2:\n      ansible_user: pi\n      ansible_python_interpreter: auto\n"
  },
  {
    "path": "playbook.yml",
    "content": "- hosts: all\n  tasks:\n    # Allows you to SSH in to the PiHole via SSH, instead of password auth, pulling from your GitHub Public key\n    - name: Ensure SSH Key is authorized\n      authorized_key:\n        user: pi\n        state: present\n        key: https://github.com/benbalter.keys\n\n    # Ensure PiHole password is not the default\n    # Here I'm using 1Password as my secret store, but you could use another source\n    - name: Change pi user password\n      become: true\n      user:\n        name: pi\n        update_password: always\n        password: \"{{ lookup('community.general.onepassword', 'PiHole', field='Pi@ login') | password_hash('sha512') }}\"\n    \n    # Update system-level dependencies\n    - name: update and upgrade apt packages\n      become: true\n      apt:\n        upgrade: dist\n        update_cache: true\n        \n    # Set Static IP of PiHole so other devices can query it for DNS lookups\n    - name: Install network manager\n      become: true\n      apt:\n        name: network-manager\n        state: present\n    - name: configure network\n      become: true\n      community.general.nmcli:\n        state: present\n        conn_name: eth0\n        ifname: eth0\n        type: ethernet\n        ip4: 192.168.1.2/24\n        gw4: 192.168.1.1\n        dns4:\n          - 1.1.1.2\n    \n    # Ensure timestamps are in my local timezone\n    - name: set timezone\n      become: true\n      community.general.timezone:\n        name: America/New_York\n        \n    # A deploy key allows you to pull (or push) from a private GitHub repo\n    - name: Ensure deploy key is present\n      community.crypto.openssh_keypair:\n        path: \"~/.ssh/id_github\"\n        type: ed25519\n      register: deploy_key\n\n    # If a new deploy key is generated, authorize it for the repo\n    # I'm using 1Password as my secret store, but you could use another source\n    - name: Ensure deploy key is authorized\n      community.general.github_deploy_key:\n        key: \"{{ deploy_key.public_key }}\"\n        name: Raspberry Pi\n        state: present\n        owner: benbalter\n        repo: pi-hole\n        token: \"{{ lookup('community.general.onepassword', 'PiHole', field='GitHub Token') }}\"\n    \n    - name: Install docker dependencies\n      become: true\n      apt:\n        name: \"{{ item }}\"\n        state: present\n        update_cache: true\n      loop:\n        - apt-transport-https\n        - ca-certificates\n        - curl\n        - gnupg\n        - lsb-release\n        - python3-pip\n        - python3-setuptools\n    - name: add Docker GPG key\n      become: true\n      apt_key:\n        url: https://download.docker.com/linux/debian/gpg\n        state: present\n    - name: add docker repository to apt\n      become: true\n      apt_repository:\n        repo: deb [arch=armhf signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian buster stable\n        state: present\n    - name: install docker\n      become: true\n      apt:\n        name: \"{{ item }}\"\n        state: present\n      loop:\n        - docker-ce\n        - docker-ce-cli\n        - containerd.io\n    - name: Add user to docker group\n      become: true\n      user:\n        name: pi\n        groups: docker\n        append: true\n    - name: Enable & Start Docker service\n      become: true\n      service:\n        name: docker\n        enabled: true\n        state: started\n    - name: Install pip components\n      pip:\n        executable: pip3\n        name:\n          - docker\n          - docker-compose\n          - virtualenv\n\n    # I version my config in a private Git Repo, so I clone it down using the deploy key\n    # Note: This will not work without modification, as it's a private repo\n    - name: Clone GitHub repo\n      git:\n        repo: git@github.com:benbalter/pi-hole.git\n        dest: /home/pi/pi-hole/\n        clone: true\n        update: true\n        key_file: ~/.ssh/id_github\n        accept_hostkey: true\n        \n    # Automatically upgrade apt packages\n    - name: install unattended upgrades\n      become: true\n      apt:\n        name: unattended-upgrades\n        state: present\n    - name: Setup unattended upgrades\n      debconf:\n        name: unattended-upgrades\n        question: unattended-upgrades/enable_auto_updates\n        vtype: boolean\n        value: \"true\"\n\n    # Prevents SSH brute force attacks\n    - name: install fail2ban\n      become: true\n      apt:\n        name: fail2ban\n        state: present\n    \n    # Install and enable NTP to ensure the clock remains accurate\n    - name: install ntp\n      become: true\n      apt:\n        name: ntp\n        state: present\n    - name: enable ntp\n      service:\n        name: ntp\n        state: started\n        enabled: true\n        \n    # Installs firewall\n    - name: install ufw\n      become: true\n      apt:\n        name: ufw\n        state: present\n\n    # Rate limits SSH attempts\n    - name: limit ssh\n      become: true\n      community.general.ufw:\n        rule: limit\n        port: ssh\n        proto: tcp\n\n    # Firewall rules\n    - name: Allow all access to SSH, DNS, and WWW\n      become: true\n      community.general.ufw:\n        rule: allow\n        app: '{{ item }}'\n      loop:\n        - SSH\n        - DNS\n        - WWW\n        - WWW Secure\n    - name: enable ufw and default to deny\n      become: true\n      ufw:\n        state: enabled\n        default: deny\n    \n    # Set PiHole (Web Admin) password, referenced above. \n    # I'm using 1Password, but you could use any secret store.\n    - name: Set Pi-Hole secret\n      copy:\n        dest: /home/pi/pi-hole/.pihole_web_password\n        content: \"{{ lookup('community.general.onepassword', 'Raspberry pi', field='password') }}\"\n        \n    - name: Set Caddy secret\n      copy:\n        dest: /home/pi/pi-hole/.caddy.env\n        # I'm using 1Password here, but you could use any secret store you wanted\n        content: \"CLOUDFLARE_API_TOKEN={{ lookup('community.general.onepassword', 'Raspberry pi', field='Cloudflare Token') }}\"\n        mode: 0700\n\n    - name: Create and start docker compose services\n      community.docker.docker_compose:\n        # Change to path to your docker-compose.yml. See below for how to clone a repo\n        project_src: /home/pi/pi-hole\n        pull: true\n        build: true\n        remove_orphans: true\n      register: output\n  \n"
  }
]