diff --git a/.gitattributes b/.gitattributes index c19aa7ab..7a9eeb6e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,5 @@ secret.yml filter=git-crypt diff=git-crypt ghcr-pull-secrets.yaml filter=git-crypt diff=git-crypt ssh-secrets.yaml filter=git-crypt diff=git-crypt guix/resources/age-key filter=git-crypt diff=git-crypt +*-secret.md filter=git-crypt diff=git-crypt +*-secrets.md filter=git-crypt diff=git-crypt diff --git a/ansible/group_vars/all/nftables.yml b/ansible/group_vars/all/nftables.yml index 22cc0840..c6851454 100644 --- a/ansible/group_vars/all/nftables.yml +++ b/ansible/group_vars/all/nftables.yml @@ -51,6 +51,9 @@ nftables_configuration: | # Allow loopback iif lo accept + # Allow all traffic from/to tailscale0 + iifname tailscale0 accept + # Allow certain inbound ICMP types (ping, traceroute). # With these allowed you are a good network citizen. meta l4proto { icmp, ipv6-icmp } counter accept diff --git a/ansible/playbook.yml b/ansible/playbook.yml index f39f4524..fc092e24 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -4,6 +4,7 @@ - linode-ips - common - pydis-mtls + - tailscale - wireguard - munin-node diff --git a/ansible/roles/postgres/templates/postgresql.conf.j2 b/ansible/roles/postgres/templates/postgresql.conf.j2 index 59d22beb..3073b5f1 100644 --- a/ansible/roles/postgres/templates/postgresql.conf.j2 +++ b/ansible/roles/postgres/templates/postgresql.conf.j2 @@ -2,7 +2,7 @@ data_directory = '/var/lib/postgresql/{{ postgres_version }}/main' hba_file = '/etc/postgresql/{{ postgres_version }}/main/pg_hba.conf' ident_file = '/etc/postgresql/{{ postgres_version }}/main/pg_ident.conf' external_pid_file = '/var/run/postgresql/{{ postgres_version }}-main.pid' -listen_addresses = '89.58.26.118,localhost' +listen_addresses = '89.58.26.118,lovelace.opossum-python.ts.net,localhost' port = 5432 unix_socket_directories = '/var/run/postgresql' diff --git a/ansible/roles/tailscale/defaults/main.yml b/ansible/roles/tailscale/defaults/main.yml new file mode 100644 index 00000000..b931ff3b --- /dev/null +++ b/ansible/roles/tailscale/defaults/main.yml @@ -0,0 +1,4 @@ +--- +tailscale_rocky_repo: "https://pkgs.tailscale.com/stable/centos/10/tailscale.repo" +tailscale_gpg_key_url: "https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg" +tailscale_apt_repo: "https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list" diff --git a/ansible/roles/tailscale/tasks/main.yml b/ansible/roles/tailscale/tasks/main.yml new file mode 100644 index 00000000..b7c71c1f --- /dev/null +++ b/ansible/roles/tailscale/tasks/main.yml @@ -0,0 +1,117 @@ +--- +- name: Add Tailscale repository (Rocky) + ansible.builtin.get_url: + url: "{{ tailscale_rocky_repo }}" + dest: /etc/yum.repos.d/tailscale.repo + mode: "0644" + when: ansible_facts["distribution"] == "Rocky" + tags: + - role::tailscale + +- name: Ensure keys directory exists (Debian) + file: + path: /usr/share/keyrings + state: directory + owner: root + group: root + mode: "0755" + when: ansible_facts["distribution"] == "Debian" + tags: + - role::tailscale + +- name: Download Tailscale GPG key (Debian) + ansible.builtin.get_url: + url: "{{ tailscale_gpg_key_url }}" + dest: /usr/share/keyrings/tailscale-archive-keyring.gpg + mode: "0644" + when: ansible_facts["distribution"] == "Debian" + tags: + - role::tailscale + +- name: Add Tailscale APT repository (Debian) + ansible.builtin.get_url: + url: "{{ tailscale_apt_repo }}" + dest: /etc/apt/sources.list.d/tailscale.list + mode: "0644" + when: ansible_facts["distribution"] == "Debian" + tags: + - role::tailscale + +- name: Update APT cache (Debian) + ansible.builtin.apt: + update_cache: yes + when: ansible_facts["distribution"] == "Debian" + tags: + - role::tailscale + +- name: Install Tailscale + package: + name: tailscale + state: present + tags: + - role::tailscale + +- name: Ensure Tailscale is enabled and started + ansible.builtin.systemd: + name: tailscaled + enabled: yes + state: started + tags: + - role::tailscale + +- name: Check if Tailscale is already authenticated + ansible.builtin.command: tailscale status --json + register: tailscale_status + failed_when: false + changed_when: false + tags: + - role::tailscale + +- name: Parse Tailscale status + ansible.builtin.set_fact: + tailscale_authenticated: "{{ tailscale_status.stdout | from_json | json_query('BackendState') not in ['NeedsLogin', 'Stopped'] }}" + tags: + - role::tailscale + +- name: Authenticate Tailscale + when: not tailscale_authenticated + ansible.builtin.command: |- + tailscale up \ + --authkey '{{ tailscale_oauth2_client_secret }}?preauthorized=true&ephemeral=false' \ + --advertise-tags '{{ tailscale_advertise_tags }}' \ + --hostname '{{ inventory_hostname }}' \ + --accept-routes \ + --accept-dns + register: tailscale_up_result + changed_when: "'Already up to date' not in tailscale_up_result.stdout" + tags: + - role::tailscale + +- name: Fetch hosted Tailscale services + ansible.builtin.command: tailscale serve get-config --all + register: tailscale_services_status + failed_when: false + changed_when: false + tags: + - role::tailscale + +- name: Parse Tailscale services + ansible.builtin.set_fact: + tailscale_hosted_services: "{{ tailscale_services_status.stdout | from_json | json_query('services') }}" + tags: + - role::tailscale + +- name: Set tailscale_hosted_services to empty list if not defined + ansible.builtin.set_fact: + tailscale_hosted_services: [] + when: not tailscale_hosted_services + tags: + - role::tailscale + +- name: Ensure Tailscale services are configured + ansible.builtin.command: |- + tailscale serve --yes --service svc:{{ item.ts_service_name }} --{{ item.proto }} {{ item.listen_port }} {{ item.proxy_dest }} + loop: "{{ tailscale_services }}" + when: "'svc:' + item.ts_service_name not in tailscale_hosted_services and item.host == inventory_hostname" + tags: + - role::tailscale diff --git a/ansible/roles/tailscale/vars/main/main.yml b/ansible/roles/tailscale/vars/main/main.yml new file mode 100644 index 00000000..334f289d --- /dev/null +++ b/ansible/roles/tailscale/vars/main/main.yml @@ -0,0 +1,12 @@ +--- +tailscale_oauth2_client_id: "{{ vault_tailscale_oauth2_client_id }}" +tailscale_oauth2_client_secret: "{{ vault_tailscale_oauth2_client_secret }}" + +tailscale_advertise_tags: "tag:baremetal" + +tailscale_services: + - host: lovelace + ts_service_name: "postgres" + proto: "tcp" + listen_port: 5432 + proxy_dest: "127.0.0.1:5432" diff --git a/ansible/roles/tailscale/vars/main/vault.yml b/ansible/roles/tailscale/vars/main/vault.yml new file mode 100644 index 00000000..2bb012f0 --- /dev/null +++ b/ansible/roles/tailscale/vars/main/vault.yml @@ -0,0 +1,13 @@ +$ANSIBLE_VAULT;1.1;AES256 +62316632633033623735393336623133363763323038323630656365363363373138626439316333 +6565656364343564393239666334613664323264663562660a366666626333666130396534663733 +37386435316135633936623961393461343765346630613064386135376530373964386338623464 +3034663939353036620a386463333639396233303332386230376164353633353631376439623136 +61346161633661323932633238393863626665663830353762323165613765313433646563656532 +64303166343534316531316539303633336433333966353038653363656163663538636464626462 +34383732346232313732336462303437346566363632653838363966653461386131633162313630 +63653733666165336363313937393034626662333833353631306238316433306164333464313664 +39333031383331393436306465636133636131316465333239363435666165643736666363353132 +36333962353639333436666334356534393033666236656261663562306436643837613733303664 +64666338653162376239643462393036626538316364396235633331336632656566643238323561 +31393130323134383462 diff --git a/kubernetes/namespaces/tailscale/README.md b/kubernetes/namespaces/tailscale/README.md new file mode 100644 index 00000000..f420762c --- /dev/null +++ b/kubernetes/namespaces/tailscale/README.md @@ -0,0 +1,20 @@ +# Tailscale + +We use the Tailscale Kubernetes Operator to allow in-cluster services to connect securely to external services via a secure tunnel. + +## Deployment + +1. Add the Helm chart `helm repo add tailscale https://pkgs.tailscale.com/helmcharts` +2. Update the Helm repo `helm repo update` +3. Install the tailscale operator, replacing OAuth credentials as necessary (from the Trust credentials section of Tailscale admin console): + ```bash + helm upgrade \ + --install \ + tailscale-operator \ + tailscale/tailscale-operator \ + --namespace=tailscale \ + --create-namespace \ + --set-string oauth.clientId="" \ + --set-string oauth.clientSecret="" \ + --wait + ``` diff --git a/kubernetes/namespaces/tailscale/services/postgres.yaml b/kubernetes/namespaces/tailscale/services/postgres.yaml new file mode 100644 index 00000000..254fba02 --- /dev/null +++ b/kubernetes/namespaces/tailscale/services/postgres.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + tailscale.com/tailnet-fqdn: postgres.opossum-python.ts.net + name: postgres + namespace: tailscale +spec: + externalName: placeholder # any value - will be overwritten by operator + type: ExternalName diff --git a/kubernetes/namespaces/tailscale/tailscale-secrets.md b/kubernetes/namespaces/tailscale/tailscale-secrets.md new file mode 100644 index 00000000..bed185c0 Binary files /dev/null and b/kubernetes/namespaces/tailscale/tailscale-secrets.md differ diff --git a/pyproject.toml b/pyproject.toml index 3b9c305e..513288f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ ansible = [ "ansible-core>=2.17.0,<3", "ansible-lint==25.2.1 ; platform_system != 'Windows'", "dnspython==2.7.0", + "jmespath==1.1.0" ] dns = [ "octodns>=1.8.0,<2", diff --git a/uv.lock b/uv.lock index 5520f16b..94ab83cf 100644 --- a/uv.lock +++ b/uv.lock @@ -566,6 +566,7 @@ ansible = [ { name = "ansible-core", version = "2.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "ansible-lint", marker = "sys_platform != 'win32'" }, { name = "dnspython" }, + { name = "jmespath" }, ] dns = [ { name = "octodns" }, @@ -590,6 +591,7 @@ ansible = [ { name = "ansible-core", specifier = ">=2.17.0,<3" }, { name = "ansible-lint", marker = "sys_platform != 'win32'", specifier = "==25.2.1" }, { name = "dnspython", specifier = "==2.7.0" }, + { name = "jmespath", specifier = "==1.1.0" }, ] dns = [ { name = "octodns", specifier = ">=1.8.0,<2" }, @@ -614,6 +616,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0"