commit d6c3872aaa7cdcc4f5632c4ebce6b05a5e99af03 Author: Finn Date: Wed Jan 17 09:45:49 2024 -0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..956d472 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/secrets diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..fa52dca --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,2 @@ +[defaults] +inventory=inventory.yml diff --git a/inventory.yml b/inventory.yml new file mode 100644 index 0000000..6444698 --- /dev/null +++ b/inventory.yml @@ -0,0 +1,16 @@ +nameservers: + hosts: + dns.janky.solutions: + ansible_host: 10.5.1.156 + powerdns_admin: yes + wireguard_ip: 10.6.0.1 + wireguard_pubkey: hYUM1LRSemvjcPfHHcH9sZOsE45xWRSkasXs8uEDJDo= + wireguard_endpoint: wg.home.finn.io + ns1.janky.zone: + ansible_host: 137.184.226.48 + wireguard_ip: 10.6.0.101 + wireguard_pubkey: TwJXoSNhKhCCerjq1P8o3SBGQEe5vfjnB2Y9uX8mATU= + ns2.janky.zone: + ansible_host: 66.42.71.31 + wireguard_ip: 10.6.0.102 + wireguard_pubkey: gTa4wsiQCGu+rbH05U8bjDJPVzINKJ/BIY0FejSWrWs= diff --git a/powerdns.md b/powerdns.md new file mode 100644 index 0000000..a9b143b --- /dev/null +++ b/powerdns.md @@ -0,0 +1,13 @@ +# PowerDNS Infrastructure + +Playbook `powerdns.yml` will do the core setup. The GUI requires some manual configuration unfortunately: +1. On first login, you will be prompted for a PowerDNS URL and password. URL is http://10.88.0.1:8081 (10.88.0.1 is the podman host IP on the default network), password is generated and written to `secrets/dns.janky.solutions/pdns-api-password.txt` +2. Navigate to Settings -> Basic and update the following settings, clicking each of their respective save buttons after updating the field (there is no global save button) + * `allow_user_create_domain` - turn on + * `allow_user_remove_domain` - turn on + * `allow_user_view_history` - turn on + * `default_domain_table_size` - `100` + * `default_record_table_size` - `1000` + * `session_timeout` - `99999` (a session timeout will trigger an SSO logout!) + * `site_name` - `Janky Solutions DNS` + * `ttl_options` - `1 minute,5 minutes,30 minutes,60 minutes,24 hours,48 hours` diff --git a/powerdns.yml b/powerdns.yml new file mode 100644 index 0000000..ea9e31a --- /dev/null +++ b/powerdns.yml @@ -0,0 +1,6 @@ +- hosts: nameservers + vars: + ansible_user: root + roles: + - base + - pdns diff --git a/roles/base/tasks/main.yml b/roles/base/tasks/main.yml new file mode 100644 index 0000000..78a08da --- /dev/null +++ b/roles/base/tasks/main.yml @@ -0,0 +1,8 @@ +- name: install common packages + apt: + name: [mosh, htop, tmux, unattended-upgrades] + +- name: remove stupid bullshit that the cloud provider may have installed + apt: + name: [ufw] + state: absent diff --git a/roles/monitoring/tasks/main.yml b/roles/monitoring/tasks/main.yml new file mode 100644 index 0000000..6ab9a41 --- /dev/null +++ b/roles/monitoring/tasks/main.yml @@ -0,0 +1,6 @@ +- name: Install monitoring tools + apt: + name: [prometheus-node-exporter, wireguard-tools] + +- name: promtail + include_tasks: promtail.yml diff --git a/roles/monitoring/tasks/promtail.yml b/roles/monitoring/tasks/promtail.yml new file mode 100644 index 0000000..b9af27e --- /dev/null +++ b/roles/monitoring/tasks/promtail.yml @@ -0,0 +1,39 @@ +- name: make /etc/apt/keyrings + file: + path: /etc/apt/keyrings + state: directory + +- name: install grafana apt key + copy: + src: grafana-apt-key.gpg + dest: /etc/apt/keyrings/grafana.gpg + +- name: add grafana apt repo + apt_repository: + repo: "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" + filename: "grafana" + +- name: install promtail + apt: + name: promtail + +- name: make /etc/systemd/system/promtail.service.d + file: + path: /etc/systemd/system/promtail.service.d + state: directory + +- name: install promtail.service override + template: + src: promtail-override.service + dest: /etc/systemd/system/promtail.service.d/override.conf + notify: + - systemctl daemon-reload + - restart promtail + +- name: install promtail config + template: + src: promtail.yml + dest: /etc/promtail/config.yml + notify: + - restart promtail + diff --git a/roles/monitoring/templates/promtail-override.service b/roles/monitoring/templates/promtail-override.service new file mode 100644 index 0000000..a79dbb6 --- /dev/null +++ b/roles/monitoring/templates/promtail-override.service @@ -0,0 +1,2 @@ +[Service] +User=root diff --git a/roles/monitoring/templates/promtail.yml b/roles/monitoring/templates/promtail.yml new file mode 100644 index 0000000..ef0a980 --- /dev/null +++ b/roles/monitoring/templates/promtail.yml @@ -0,0 +1,33 @@ +server: + log_level: warn + http_listen_port: 0 + grpc_listen_port: 0 + +clients: + - url: https://loki.callpipe.com/loki/api/v1/push + external_labels: + hostname: "{{ inventory_hostname }}" + tls_config: + ca_file: /etc/step/certs/root_ca.crt + cert_file: /etc/step/certs/callpipe.crt + key_file: /etc/step/certs/callpipe.key + +scrape_configs: + - job_name: journald + journal: + labels: + job: systemd-label + relabel_configs: + - source_labels: ['__journal__systemd_unit'] + target_label: 'unit' +{% if 'jobs' in logs %} +{% for job_name, path in logs.jobs.items() %} + - job_name: {{ job_name }} + static_configs: + - targets: + - localhost + labels: + job: {{ job_name }} + __path__: {{ path }} +{% endfor %} +{% endif %} diff --git a/roles/pdns/handlers/main.yml b/roles/pdns/handlers/main.yml new file mode 100644 index 0000000..0096c8b --- /dev/null +++ b/roles/pdns/handlers/main.yml @@ -0,0 +1,137 @@ +- name: systemctl daemon-reload + command: systemctl daemon-reload + +- name: restart systemd-resolved + service: + name: systemd-resolved + state: restarted + +- name: restart marmot + service: + name: marmot + state: restarted + +- name: restart postgresql + service: + name: postgresql + state: restarted + +- name: create db schema + command: psql pdns + args: + stdin: | + CREATE TABLE domains ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + master VARCHAR(128) DEFAULT NULL, + last_check INT DEFAULT NULL, + type TEXT NOT NULL, + notified_serial BIGINT DEFAULT NULL, + account VARCHAR(40) DEFAULT NULL, + options TEXT DEFAULT NULL, + catalog TEXT DEFAULT NULL, + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE UNIQUE INDEX name_index ON domains(name); + CREATE INDEX catalog_idx ON domains(catalog); + + + CREATE TABLE records ( + id BIGSERIAL PRIMARY KEY, + domain_id INT DEFAULT NULL, + name VARCHAR(255) DEFAULT NULL, + type VARCHAR(10) DEFAULT NULL, + content VARCHAR(65535) DEFAULT NULL, + ttl INT DEFAULT NULL, + prio INT DEFAULT NULL, + disabled BOOL DEFAULT 'f', + ordername VARCHAR(255), + auth BOOL DEFAULT 't', + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE INDEX rec_name_index ON records(name); + CREATE INDEX nametype_index ON records(name,type); + CREATE INDEX domain_id ON records(domain_id); + CREATE INDEX recordorder ON records (domain_id, ordername text_pattern_ops); + + + CREATE TABLE supermasters ( + ip INET NOT NULL, + nameserver VARCHAR(255) NOT NULL, + account VARCHAR(40) NOT NULL, + PRIMARY KEY(ip, nameserver) + ); + + + CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + domain_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) DEFAULT NULL, + comment VARCHAR(65535) NOT NULL, + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE INDEX comments_domain_id_idx ON comments (domain_id); + CREATE INDEX comments_name_type_idx ON comments (name, type); + CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); + + + CREATE TABLE domainmetadata ( + id SERIAL PRIMARY KEY, + domain_id INT REFERENCES domains(id) ON DELETE CASCADE, + kind VARCHAR(32), + content TEXT + ); + + CREATE INDEX domainidmetaindex ON domainmetadata(domain_id); + + + CREATE TABLE cryptokeys ( + id SERIAL PRIMARY KEY, + domain_id INT REFERENCES domains(id) ON DELETE CASCADE, + flags INT NOT NULL, + active BOOL, + published BOOL DEFAULT TRUE, + content TEXT + ); + + CREATE INDEX domainidindex ON cryptokeys(domain_id); + + + CREATE TABLE tsigkeys ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + algorithm VARCHAR(50), + secret VARCHAR(255), + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) + ); + + CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm); + become: true + become_user: postgres + +- name: restart pdns + service: + name: pdns + state: restarted + +- name: restart powerdns-admin + service: + name: powerdns-admin + state: restarted + +- name: restart wg-quick@wg0 + service: + name: wg-quick@wg0 + state: restarted diff --git a/roles/pdns/tasks/main.yml b/roles/pdns/tasks/main.yml new file mode 100644 index 0000000..9cbb465 --- /dev/null +++ b/roles/pdns/tasks/main.yml @@ -0,0 +1,74 @@ +- name: install stuff from apt + apt: + name: [pdns-server, pdns-backend-pgsql, wireguard-tools, python3-psycopg2, postgresql] + +- name: configure wireguard tunnel + template: + src: wireguard.conf + dest: /etc/wireguard/wg0.conf + notify: + - restart wg-quick@wg0 + +- name: enable the wireguard tunnel + service: + name: wg-quick@wg0 + enabled: true + +- name: check if resolved is installed + stat: + path: /etc/systemd/resolved.conf + register: resolvedconf + +- name: create resolved.conf.d + file: + path: /etc/systemd/resolved.conf.d + state: directory + when: resolvedconf.stat.exists + +- name: disable systemd-resolved stub listener (its probably using port 53 and we need it) + template: + src: systemd-resolved.conf + dest: /etc/systemd/resolved.conf.d/10-disable-stub-listener.conf + notify: + - restart systemd-resolved + when: resolvedconf.stat.exists + +- name: configure postgres for streaming replication + template: + src: postgres.conf + dest: /etc/postgresql/15/main/conf.d/replication.conf + notify: + - restart postgresql + +- name: configure postgres remote access + community.postgresql.postgresql_pg_hba: + address: 10.6.0.0/24 + contype: host + databases: pdns + dest: /etc/postgresql/15/main/pg_hba.conf + notify: + - restart postgresql + when: powerdns_admin|default(false) + +- meta: flush_handlers + +- include_tasks: + file: postgresql-write.yml + apply: + become: true + become_user: postgres + when: powerdns_admin|default(false) + +- include_tasks: + file: postgresql-read.yml + apply: + become: true + become_user: postgres + when: not powerdns_admin|default(false) + +- include_tasks: powerdns.yml + +- meta: flush_handlers + +- include_tasks: powerdns-admin.yml + when: powerdns_admin|default(false) diff --git a/roles/pdns/tasks/postgresql-read.yml b/roles/pdns/tasks/postgresql-read.yml new file mode 100644 index 0000000..192aba6 --- /dev/null +++ b/roles/pdns/tasks/postgresql-read.yml @@ -0,0 +1,36 @@ +- name: create db in postgres + community.postgresql.postgresql_db: + name: pdns + notify: + - create db schema + +- meta: flush_handlers # schema must be created before permission grants happen + +- name: create postgres pdns user + community.postgresql.postgresql_user: + name: pdns + db: pdns + password: "{{ lookup('ansible.builtin.password', 'secrets/' + inventory_hostname + '/pg-pdns-password.txt', length=15) }}" + +- name: grant postgres pdns user permissions + community.postgresql.postgresql_privs: + database: pdns + roles: pdns + type: "{{ item }}" + privs: all + objs: ALL_IN_SCHEMA + with_items: ["table", "sequence"] + +- name: create subscription + community.postgresql.postgresql_subscription: + db: pdns + name: pdns_{{ ansible_hostname }} + publications: pdns + connparams: + host: 10.6.0.1 + port: 5432 + user: "replication" + password: "{{ lookup('ansible.builtin.password', 'secrets/pg-replication-password.txt', length=15) }}" + dbname: pdns + subsparams: + copy_data: true diff --git a/roles/pdns/tasks/postgresql-write.yml b/roles/pdns/tasks/postgresql-write.yml new file mode 100644 index 0000000..302771b --- /dev/null +++ b/roles/pdns/tasks/postgresql-write.yml @@ -0,0 +1,42 @@ +- name: create db in postgres + community.postgresql.postgresql_db: + name: pdns + notify: + - create db schema + +- meta: flush_handlers # schema must be created before permission grants happen + +- name: create postgres pdns user + community.postgresql.postgresql_user: + name: pdns + db: pdns + password: "{{ lookup('ansible.builtin.password', 'secrets/' + inventory_hostname + '/pg-pdns-password.txt', length=15) }}" + +- name: grant postgres pdns user permissions + community.postgresql.postgresql_privs: + database: pdns + roles: pdns + type: "{{ item }}" + privs: all + objs: ALL_IN_SCHEMA + with_items: ["table", "sequence"] + +- name: create postgres replication user + community.postgresql.postgresql_user: + name: replication + password: "{{ lookup('ansible.builtin.password', 'secrets/pg-replication-password.txt', length=15) }}" + role_attr_flags: replication + +- name: grant postgres replication user permissions + community.postgresql.postgresql_privs: + database: pdns + roles: replication + type: "{{ item }}" + privs: all + objs: ALL_IN_SCHEMA + with_items: ["table", "sequence"] + +- name: create postgresql publication + community.postgresql.postgresql_publication: + db: pdns + name: pdns diff --git a/roles/pdns/tasks/powerdns-admin.yml b/roles/pdns/tasks/powerdns-admin.yml new file mode 100644 index 0000000..56ea9f9 --- /dev/null +++ b/roles/pdns/tasks/powerdns-admin.yml @@ -0,0 +1,23 @@ +- name: install podman + apt: + name: [podman] + +- name: install powerdns-admin service + template: + src: powerdns-admin.service + dest: /etc/systemd/system/powerdns-admin.service + notify: + - systemctl daemon-reload + - restart powerdns-admin + +- name: configure powerdns-admin + template: + src: powerdns-admin.env + dest: /etc/powerdns-admin.env + notify: + - restart powerdns-admin + +- name: enable powerdns-admin + service: + name: powerdns-admin + enabled: true diff --git a/roles/pdns/tasks/powerdns.yml b/roles/pdns/tasks/powerdns.yml new file mode 100644 index 0000000..c2b0434 --- /dev/null +++ b/roles/pdns/tasks/powerdns.yml @@ -0,0 +1,13 @@ +- name: configure powerdns + template: + src: powerdns.conf + dest: /etc/powerdns/pdns.d/config.conf + notify: + - restart pdns + +- name: disable powerdns bind backend + file: + path: /etc/powerdns/pdns.d/bind.conf + state: absent + notify: + - restart pdns diff --git a/roles/pdns/templates/pg_hpa.conf b/roles/pdns/templates/pg_hpa.conf new file mode 100644 index 0000000..6d99ec8 --- /dev/null +++ b/roles/pdns/templates/pg_hpa.conf @@ -0,0 +1,23 @@ +# DO NOT DISABLE! +# If you change this first entry you will need to make sure that the +# database superuser can access the database using some other method. +# Noninteractive access to all databases is required during automatic +# maintenance (custom daily cronjobs, replication, and similar tasks). +# +# Database administrative login by Unix domain socket +local all postgres peer + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all peer +# IPv4 local connections: +host all all 127.0.0.1/32 scram-sha-256 +# IPv6 local connections: +host all all ::1/128 scram-sha-256 +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all peer +host replication all 127.0.0.1/32 scram-sha-256 +host replication all ::1/128 scram-sha-256 +host all all 10.6.0.0/24 md5 diff --git a/roles/pdns/templates/postgres.conf b/roles/pdns/templates/postgres.conf new file mode 100644 index 0000000..f027418 --- /dev/null +++ b/roles/pdns/templates/postgres.conf @@ -0,0 +1,7 @@ +listen_addresses = 'localhost,{{ wireguard_ip }}' + +{% if powerdns_admin|default(false) %} +# write replica specific settings +wal_level = logical +max_wal_senders = 5 +{% endif %} diff --git a/roles/pdns/templates/powerdns-admin.env b/roles/pdns/templates/powerdns-admin.env new file mode 100644 index 0000000..d1828d4 --- /dev/null +++ b/roles/pdns/templates/powerdns-admin.env @@ -0,0 +1,13 @@ +SECRET_KEY={{ lookup('ansible.builtin.ini', 'pdns_admin_secret section=pdns file=secrets/' + inventory_hostname + '.ini') }} +OIDC_OAUTH_ENABLED=true +OIDC_OAUTH_KEY=powerdnsadmin +OIDC_OAUTH_SECRET={{ lookup('ansible.builtin.ini', 'oidc_secret section=pdns file=secrets/' + inventory_hostname + '.ini') }} +OIDC_OAUTH_API_URL=https://auth.janky.solutions/auth/realms/janky.solutions/protocol/openid-connect/ +OIDC_OAUTH_METADATA_URL=https://auth.janky.solutions/auth/realms/janky.solutions/.well-known/openid-configuration +OIDC_OAUTH_LOGOUT_URL=https://auth.janky.solutions/auth/realms/janky.solutions/protocol/openid-connect/logout +OIDC_OAUTH_USERNAME=preferred_username +OIDC_OAUTH_FIRSTNAME=given_name +OIDC_OAUTH_LAST_NAME=family_name +OIDC_OAUTH_EMAIL=email +SIGNUP_ENABLED=false +LOCAL_DB_ENABLED=false diff --git a/roles/pdns/templates/powerdns-admin.service b/roles/pdns/templates/powerdns-admin.service new file mode 100644 index 0000000..e8d4234 --- /dev/null +++ b/roles/pdns/templates/powerdns-admin.service @@ -0,0 +1,12 @@ +[Unit] +Description=PowerDNS Admin +Wants=network.target + +[Service] +Type=simple +ExecStartPre=/usr/bin/podman pull docker.io/powerdnsadmin/pda-legacy:latest +ExecStart=/usr/bin/podman run --rm -v pda-data:/data -p 9191:80 --env-file /etc/powerdns-admin.env --name powerdns-admin docker.io/powerdnsadmin/pda-legacy:latest +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/roles/pdns/templates/powerdns.conf b/roles/pdns/templates/powerdns.conf new file mode 100644 index 0000000..272c03e --- /dev/null +++ b/roles/pdns/templates/powerdns.conf @@ -0,0 +1,16 @@ +launch=gpgsql +gpgsql-host=localhost +gpgsql-port=5432 +gpgsql-dbname=pdns +gpgsql-user=pdns +gpgsql-password={{ lookup('ansible.builtin.password', 'secrets/' + inventory_hostname + '/pg-pdns-password.txt', length=15) }} +gpgsql-dnssec=yes +default-soa-content=ns1.janky.zone dns-admin.@ 0 10800 3600 604800 3600 + +{% if powerdns_admin | default(false) %} +api=yes +api-key={{ lookup('ansible.builtin.password', 'secrets/' + inventory_hostname + '/pdns-api-password.txt', length=15) }} +webserver-address=10.88.0.1 +webserver-allow-from=10.88.0.0/24 +{% endif %} +q diff --git a/roles/pdns/templates/systemd-resolved.conf b/roles/pdns/templates/systemd-resolved.conf new file mode 100644 index 0000000..6e95967 --- /dev/null +++ b/roles/pdns/templates/systemd-resolved.conf @@ -0,0 +1,2 @@ +[Resolve] +DNSStubListener=no diff --git a/roles/pdns/templates/wireguard.conf b/roles/pdns/templates/wireguard.conf new file mode 100644 index 0000000..a29122d --- /dev/null +++ b/roles/pdns/templates/wireguard.conf @@ -0,0 +1,14 @@ +[Interface] +PrivateKey = {{ lookup('ansible.builtin.ini', 'private_key section=wireguard file=secrets/' + inventory_hostname + '.ini') }} +ListenPort = 51822 +Address = {{ wireguard_ip }} + +{% for host in hostvars %} +{% if host != inventory_hostname %} +# {{ host }} +[Peer] +Endpoint = {{ hostvars[host].wireguard_endpoint|default(host) }}:51822 +PublicKey = {{ hostvars[host].wireguard_pubkey }} +AllowedIPs = {{ hostvars[host].wireguard_ip }} + +{% endif %}{% endfor %}