Initial IPv6 delployment #2

Merged
CaZzzer merged 2 commits from feature/ipv6 into master 2023-04-04 08:56:13 +00:00
24 changed files with 254 additions and 56 deletions

1
.idea/alpina.iml generated
View File

@ -28,6 +28,7 @@
<option value="$MODULE_DIR$/roles/nextcloud/templates" /> <option value="$MODULE_DIR$/roles/nextcloud/templates" />
<option value="$MODULE_DIR$/roles/arrstack/templates" /> <option value="$MODULE_DIR$/roles/arrstack/templates" />
<option value="$MODULE_DIR$/roles/jellyfin/templates" /> <option value="$MODULE_DIR$/roles/jellyfin/templates" />
<option value="$MODULE_DIR$/roles/docker_host/templates" />
</list> </list>
</option> </option>
</component> </component>

23
.idea/jsonSchemas.xml generated
View File

@ -3,6 +3,24 @@
<component name="JsonSchemaMappingsProjectConfiguration"> <component name="JsonSchemaMappingsProjectConfiguration">
<state> <state>
<map> <map>
<entry key="Ansible Tasks File">
<value>
<SchemaInfo>
<option name="name" value="Ansible Tasks File" />
<option name="relativePathToSchema" value="https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/ansible.json#/$defs/tasks" />
<option name="applicationDefined" value="true" />
<option name="patterns">
<list>
<Item>
<option name="pattern" value="true" />
<option name="path" value="*/tasks/*.yml" />
<option name="mappingKind" value="Pattern" />
</Item>
</list>
</option>
</SchemaInfo>
</value>
</entry>
<entry key="Traefik v2"> <entry key="Traefik v2">
<value> <value>
<SchemaInfo> <SchemaInfo>
@ -45,6 +63,11 @@
<option name="applicationDefined" value="true" /> <option name="applicationDefined" value="true" />
<option name="patterns"> <option name="patterns">
<list> <list>
<Item>
<option name="pattern" value="true" />
<option name="path" value="*/docker-compose.yml" />
<option name="mappingKind" value="Pattern" />
</Item>
<Item> <Item>
<option name="pattern" value="true" /> <option name="pattern" value="true" />
<option name="path" value="*/docker-compose.yml.j2" /> <option name="path" value="*/docker-compose.yml.j2" />

View File

@ -1,4 +1,6 @@
.POSIX: .POSIX:
.PHONY: *
.EXPORT_ALL_VARIABLES:
env ?= staging env ?= staging
vault_id ?= alpina@contrib/rbw-client.sh vault_id ?= alpina@contrib/rbw-client.sh

View File

@ -2,3 +2,17 @@
A home for configuring all of my homelab containers on a Debian Linux machine. A home for configuring all of my homelab containers on a Debian Linux machine.
This assumes a Debian Linux machine with Docker and Docker Compose installed. This assumes a Debian Linux machine with Docker and Docker Compose installed.
# Notes
## IPv6
The current configuration is designed to work with IPv6.
However, because of how (not properly) I'm doing the subnetting
from the host's network, NDP doesn't work.
This means that container IPs are not accessible from other hosts on the local network.
I simply have a static route on my router to the container subnet,
that uses the IP of this host as the gateway.
This is a limitation of my current ISP, I only have a single /64 subnet for my lab network.
I'd like to get a /56 or /48, perhaps using Hurricane Electric's tunnel broker.
*Sigh* ISPs being stingy with the 2^48 prefixes they're afraid of running out of.

View File

@ -1,3 +1,11 @@
{% macro default_network(subnet_index) %}
default:
enable_ipv6: true
ipam:
config:
- subnet: {{ docker_ipv6_subnet | ansible.utils.ipsubnet(80, subnet_index) }}
{% endmacro %}
{% macro traefik_labels(host, service="", port="", auth=false) %} {% macro traefik_labels(host, service="", port="", auth=false) %}
traefik.enable=true traefik.enable=true
- traefik.http.routers.{{ host }}.rule=Host(`{{ host }}.{{ domain }}`) - traefik.http.routers.{{ host }}.rule=Host(`{{ host }}.{{ domain }}`)

View File

@ -28,8 +28,12 @@
when: item.state == "file" when: item.state == "file"
- name: Deploy docker-compose for {{ current_svc_name }} - name: Deploy docker-compose for {{ current_svc_name }}
community.docker.docker_compose: command: docker compose -f "{{ current_svc_path }}/docker-compose.yml" up -d --pull --remove-orphans
project_src: "{{ current_svc_path }}" register: docker_compose_output
state: present # Not perfect idempotency, but the built-in docker_compose module doesn't support docker-compose v2
pull: true # And of course there's an IPv6 bug in docker-compose v1, smh
remove_orphans: true # https://github.com/docker/compose/issues/7670
changed_when: "'created' in docker_compose_output.stderr.lower()"
- debug:
var: docker_compose_output

View File

@ -0,0 +1 @@
docker_ipv6_index: 255

View File

@ -0,0 +1 @@
docker_ipv6_index: 254

14
poetry.lock generated
View File

@ -254,6 +254,18 @@ files = [
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
] ]
[[package]]
name = "netaddr"
version = "0.8.0"
description = "A network address manipulation library for Python"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
{file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.0" version = "23.0"
@ -366,4 +378,4 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "4c3656f66006d184debf3777b8df073898df0eb1f53611cdd47ec4c543071595" content-hash = "e5cad99dc808b0751037fa8d524f2a4c4eac8f6ae5710c6ed5f32def518746b9"

View File

@ -9,6 +9,7 @@ readme = "README.md"
python = "^3.10" python = "^3.10"
ansible = "^7.3.0" ansible = "^7.3.0"
ansible-vault = "^2.1.0" ansible-vault = "^2.1.0"
netaddr = "^0.8.0"
[build-system] [build-system]

View File

@ -1,6 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -x
apk add --no-cache wireguard-tools-wg
local_gateway=$(ip route | grep default | awk '{print $3}') local_gateway=$(ip route | grep default | awk '{print $3}')
# This used as the gateway address for NAT-PMP to work properly # This used as the gateway address for NAT-PMP to work properly
@ -19,5 +18,9 @@ ip route add default via "$wg_gateway"
# Note that the DNS isn't changed, so there's actually a leak there # Note that the DNS isn't changed, so there's actually a leak there
# That's on purpose, just in case I want to access local jackett from qbit # That's on purpose, just in case I want to access local jackett from qbit
# Still need to figure out how to make this work with IPv6
# Prevent IPv6 leaks
# ip -6 route del default
# Finally, optionally allow access to the home network # Finally, optionally allow access to the home network
# ip route add "\{\{ home_network }}" via "$local_gateway" # ip route add "\{\{ home_network }}" via "$local_gateway"

View File

@ -1,9 +1,11 @@
{% from "contrib/compose_helpers.j2" import traefik_labels with context %} {% import 'contrib/compose_helpers.j2' as helpers with context %}
{##} {##}
version: "3.7" version: "3.9"
networks: networks:
default: {# {{ helpers.default_network(249) | indent(2) }}#}
# TODO: Figure out IPv6 leaks
ipv4_only:
traefik_traefik: traefik_traefik:
external: true external: true
@ -14,10 +16,13 @@ services:
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
labels: labels:
- {{ traefik_labels("qbit", port="8080", auth=true) | indent(6) }} - {{ helpers.traefik_labels('qbit', port='8080', auth=true) | indent(6) }}
restart: unless-stopped restart: unless-stopped
environment:
- DOCKER_MODS=linuxserver/mods:universal-package-install
- INSTALL_PACKAGES=wireguard-tools-wg
networks: networks:
- default - ipv4_only
- traefik_traefik - traefik_traefik
volumes: volumes:
- ./wireguard:/etc/wireguard:ro - ./wireguard:/etc/wireguard:ro
@ -31,12 +36,12 @@ services:
image: linuxserver/prowlarr:latest image: linuxserver/prowlarr:latest
container_name: prowlarr container_name: prowlarr
labels: labels:
- {{ traefik_labels("prowlarr", port="9696", auth=true) | indent(6) }} - {{ helpers.traefik_labels('prowlarr', port='9696', auth=true) | indent(6) }}
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- qbittorrent - qbittorrent
networks: networks:
- default - ipv4_only
- traefik_traefik - traefik_traefik
volumes: volumes:
- {{ base_volume_path }}/arrstack/config/prowlarr:/config - {{ base_volume_path }}/arrstack/config/prowlarr:/config
@ -45,12 +50,12 @@ services:
image: linuxserver/sonarr:latest image: linuxserver/sonarr:latest
container_name: sonarr container_name: sonarr
labels: labels:
- {{ traefik_labels("sonarr", port="8989", auth=true) | indent(6) }} - {{ helpers.traefik_labels('sonarr', port='8989', auth=true) | indent(6) }}
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- qbittorrent - qbittorrent
networks: networks:
- default - ipv4_only
- traefik_traefik - traefik_traefik
volumes: volumes:
- {{ base_volume_path }}/arrstack/config/sonarr:/config - {{ base_volume_path }}/arrstack/config/sonarr:/config
@ -61,12 +66,12 @@ services:
image: linuxserver/radarr:latest image: linuxserver/radarr:latest
container_name: radarr container_name: radarr
labels: labels:
- {{ traefik_labels("radarr", port="7878", auth=true) | indent(6) }} - {{ helpers.traefik_labels('radarr', port='7878', auth=true) | indent(6) }}
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- qbittorrent - qbittorrent
networks: networks:
- default - ipv4_only
- traefik_traefik - traefik_traefik
volumes: volumes:
- {{ base_volume_path }}/arrstack/config/radarr:/config - {{ base_volume_path }}/arrstack/config/radarr:/config

View File

@ -7,5 +7,5 @@ PrivateKey = {{ wg_privkey }}
[Peer] [Peer]
PublicKey = {{ wg_peer_pubkey }} PublicKey = {{ wg_peer_pubkey }}
AllowedIPs = 0.0.0.0/0 AllowedIPs = 0.0.0.0/0,::0/0
Endpoint = {{ wg_peer_endpoint }} Endpoint = {{ wg_peer_endpoint }}

View File

@ -1,9 +1,9 @@
{% from "contrib/compose_helpers.j2" import traefik_labels with context %} {% import 'contrib/compose_helpers.j2' as helpers with context %}
{##} {##}
version: "3.7" version: "3.9"
networks: networks:
default: {{ helpers.default_network(253) | indent(2) }}
traefik_traefik: traefik_traefik:
external: true external: true
@ -12,11 +12,14 @@ services:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION} image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION}
container_name: authentik_server container_name: authentik_server
labels: labels:
- {{ traefik_labels("auth", port="9000") | indent(6) }} - {{ helpers.traefik_labels('auth', port='9000') | indent(6) }}
- traefik.http.middlewares.authentik.forwardauth.address=http://authentik_server:9000/outpost.goauthentik.io/auth/traefik - traefik.http.middlewares.authentik.forwardauth.address=http://localhost:9000/outpost.goauthentik.io/auth/traefik
- traefik.http.middlewares.authentik.forwardauth.trustForwardHeader=true - traefik.http.middlewares.authentik.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version - traefik.http.middlewares.authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
restart: unless-stopped restart: unless-stopped
# Port forward is needed because traefik can't resolve the container name from the host network
ports:
- "9000:9000"
command: server command: server
env_file: env_file:
- .env.authentik - .env.authentik
@ -61,3 +64,5 @@ services:
interval: 30s interval: 30s
retries: 5 retries: 5
timeout: 3s timeout: 3s
volumes:
- {{ base_volume_path }}/authentik/redis:/data

View File

@ -1,3 +1,12 @@
- name: Install Debian packages
become: yes
ansible.builtin.apt:
name:
- docker-ce
- docker-compose-plugin
- firewalld
state: latest
- name: Upgrade Debian packages - name: Upgrade Debian packages
become: yes become: yes
ansible.builtin.apt: ansible.builtin.apt:
@ -8,6 +17,44 @@
state: latest state: latest
register: apt_upgrades register: apt_upgrades
- name: Allow SSH
become: yes
firewalld:
service: ssh
permanent: yes
state: enabled
immediate: yes
- name: Allow Web
become: yes
firewalld:
service: http
permanent: yes
state: disabled
immediate: yes
- name: Allow Web Secure
become: yes
firewalld:
service: https
permanent: yes
state: enabled
immediate: yes
- name: Allow 443 udp for http3
become: yes
firewalld:
port: 443/udp
permanent: yes
state: enabled
immediate: yes
- name: Enable Firewall
become: yes
firewalld:
state: enabled
immediate: yes
- name: Reboot if needed - name: Reboot if needed
become: yes become: yes
ansible.builtin.reboot: ansible.builtin.reboot:

View File

@ -3,3 +3,41 @@
state: directory state: directory
path: "{{ my_svc_path }}" path: "{{ my_svc_path }}"
mode: "700" mode: "700"
- name: Get IPv6 subnet for Docker
set_fact:
docker_ipv6_subnet: "{{ \
ansible_default_ipv6.address \
| ansible.utils.ipsubnet(64) \
| ansible.utils.ipsubnet(72, docker_ipv6_index) \
}}"
- debug:
var: docker_ipv6_subnet
- name: Configure Docker daemon
become: yes
template:
src: "daemon.json.j2"
dest: "/etc/docker/daemon.json"
owner: root
group: root
mode: "0644"
register: docker_daemon_config
- name: Remove docker0 from firewalld trusted zone
become: yes
firewalld:
zone: trusted
interface: docker0
permanent: yes
immediate: yes
state: disabled
register: docker0_firewalld
- name: Restart Docker daemon
become: yes
service:
name: docker
state: restarted
when: docker_daemon_config.changed or docker0_firewalld.changed

View File

@ -0,0 +1,4 @@
{
"ipv6": true,
"fixed-cidr-v6": "{{ docker_ipv6_subnet | ansible.utils.ipsubnet(80, 0) }}"
}

View File

@ -1,9 +1,9 @@
{% from "contrib/compose_helpers.j2" import traefik_labels with context %} {% import 'contrib/compose_helpers.j2' as helpers with context %}
{##} {##}
version: "3.7" version: "3.9"
networks: networks:
default: {{ helpers.default_network(252) | indent(2) }}
traefik_traefik: traefik_traefik:
external: true external: true
@ -12,7 +12,7 @@ services:
image: gitea/gitea:1.18 image: gitea/gitea:1.18
container_name: gitea_server container_name: gitea_server
labels: labels:
- {{ traefik_labels("gitea", port="3000") | indent(6) }} - {{ helpers.traefik_labels('gitea', port='3000') | indent(6) }}
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env.gitea - .env.gitea

View File

@ -1,9 +1,9 @@
{% from "contrib/compose_helpers.j2" import traefik_labels with context %} {% import 'contrib/compose_helpers.j2' as helpers with context %}
{##} {##}
version: "3.7" version: "3.9"
networks: networks:
default: {{ helpers.default_network(250) | indent(2) }}
traefik_traefik: traefik_traefik:
external: true external: true
@ -12,7 +12,7 @@ services:
image: jellyfin/jellyfin:10.8.6 image: jellyfin/jellyfin:10.8.6
container_name: jellyfin_jellyfin container_name: jellyfin_jellyfin
labels: labels:
- {{ traefik_labels("jellyfin", port="8096") | indent(6) }} - {{ helpers.traefik_labels('jellyfin', port='8096') | indent(6) }}
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env.jellyfin - .env.jellyfin

View File

@ -1,9 +1,9 @@
{% from "contrib/compose_helpers.j2" import traefik_labels with context %} {% import 'contrib/compose_helpers.j2' as helpers with context %}
{##} {##}
version: "3.7" version: "3.9"
networks: networks:
default: {{ helpers.default_network(251) | indent(2) }}
traefik_traefik: traefik_traefik:
external: true external: true
@ -83,7 +83,7 @@ services:
image: nginx:1.23-alpine image: nginx:1.23-alpine
container_name: nextcloud_web container_name: nextcloud_web
labels: labels:
- {{ traefik_labels("nc") | indent(6) }} - {{ helpers.traefik_labels('nc') | indent(6) }}
restart: unless-stopped restart: unless-stopped
links: links:
- app - app

View File

@ -1,35 +1,37 @@
{% from "contrib/compose_helpers.j2" import traefik_labels with context %} {% import 'contrib/compose_helpers.j2' as helpers with context %}
{##} {##}
version: "3.7" version: "3.9"
networks: networks:
default:
traefik: traefik:
internal: true internal: true
enable_ipv6: true
ipam: ipam:
config: config:
- subnet: {{ traefik_ip }}/24 - subnet: {{ traefik_ip }}/24
- subnet: {{ docker_ipv6_subnet | ansible.utils.ipsubnet(80, 255) }}
services: services:
traefik: traefik:
image: traefik:v2.9 image: traefik:v2.9
container_name: traefik container_name: traefik
labels:
- {{ traefik_labels("traefik", service="api@internal") | indent(6) }}
restart: unless-stopped restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8080:8080"
env_file: env_file:
- .env.traefik - .env.traefik
networks: network_mode: host
default:
traefik:
ipv4_address: {{ traefik_ip }}
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro - ./traefik.yml:/etc/traefik/traefik.yml:ro
- {{ base_volume_path }}/traefik/rules:/rules:ro - ./rules:/rules:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- {{ base_volume_path }}/traefik/rules:/rules/extra:ro
- {{ base_volume_path }}/traefik/logs:/logs - {{ base_volume_path }}/traefik/logs:/logs
- {{ base_volume_path }}/traefik/acme:/acme - {{ base_volume_path }}/traefik/acme:/acme
# This is mostly just so that the traefik network gets created
whoami:
image: containous/whoami
container_name: whoami
labels:
- {{ helpers.traefik_labels('whoami', port=80) | indent(6) }}
networks:
- traefik

View File

@ -0,0 +1,25 @@
http:
routers:
traefik-dash:
rule: "Host(`traefik.{{ domain }}`)"
entryPoints:
- web
service: traefik-dash
traefik-dash-tls:
rule: "Host(`traefik.{{ domain }}`)"
entryPoints:
- websecure
service: traefik-dash
tls:
certResolver: letsencrypt
domains:
- main: "{{ domain }}"
sans:
- "*.{{ domain }}"
services:
traefik-dash:
loadBalancer:
servers:
- url: "http://localhost:8080"

View File

@ -8,14 +8,16 @@ accessLog:
filePath: /logs/access.log filePath: /logs/access.log
bufferingSize: 100 bufferingSize: 100
experimental:
http3: true
entryPoints: entryPoints:
web: web:
address: ":80" address: ":80"
forwardedHeaders:
trustedIPs:
- "172.16.0.0/12"
websecure: websecure:
address: ":443" address: ":443"
http3:
advertisedPort: 443
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:

View File

@ -3,9 +3,9 @@
- docker_host - docker_host
- traefik - traefik
- authentik - authentik
- gitea
- nextcloud - nextcloud
- jellyfin - jellyfin
- gitea
- arrstack - arrstack
post_tasks: post_tasks:
- name: Docker prune objects - name: Docker prune objects