diff --git a/ansible/group_vars/bind9_servers/main.yml b/ansible/group_vars/bind9_servers/main.yml index 88d43f6..a20015e 100644 --- a/ansible/group_vars/bind9_servers/main.yml +++ b/ansible/group_vars/bind9_servers/main.yml @@ -14,12 +14,12 @@ bind_dnssec_validation: auto # Secondaries that should receive NOTIFY bind_also_notify: - 204.87.183.53 # ns2 - - 172.245.56.83 # vm1.w5isp.com + - 172.245.56.83 # git.mcintire.me bind_allow_transfer: - 23.128.97.53 # ns-global.kjsl.com - 204.87.183.53 # ns2 - - 172.245.56.83 # vm1.w5isp.com + - 172.245.56.83 # git.mcintire.me # Zone definitions are loaded from individual files in zones/ directory bind_zones: diff --git a/ansible/group_vars/bind9_servers/mcintire.me.yml b/ansible/group_vars/bind9_servers/mcintire.me.yml index e85d97c..8316ea8 100644 --- a/ansible/group_vars/bind9_servers/mcintire.me.yml +++ b/ansible/group_vars/bind9_servers/mcintire.me.yml @@ -20,6 +20,8 @@ mcintire_me_zone: ip: 204.110.191.8 - name: git ip: 172.245.56.83 + - name: vm1 + ip: 172.245.56.83 other_name_servers: - ha.mcintire.me. IN CNAME q1l09qiycnaagngmjsg1qas0rhqcfoou.ui.nabu.casa. - _acme-challenge.ha.mcintire.me. IN CNAME _acme-challenge.q1l09qiycnaagngmjsg1qas0rhqcfoou.ui.nabu.casa. diff --git a/ansible/group_vars/bind9_servers/w5isp.com.yml b/ansible/group_vars/bind9_servers/w5isp.com.yml index daa8e3a..dc1caca 100644 --- a/ansible/group_vars/bind9_servers/w5isp.com.yml +++ b/ansible/group_vars/bind9_servers/w5isp.com.yml @@ -30,6 +30,8 @@ w5isp_com_zone: ip: 204.110.191.8 - name: vaprs ip: 204.110.191.8 + - name: aprs + ip: 204.110.191.232 other_name_servers: - autodiscover.w5isp.com. IN CNAME mail.w5isp.com. - autoconfig.w5isp.com. IN CNAME mail.w5isp.com. diff --git a/ansible/group_vars/proxmox_servers.yml b/ansible/group_vars/proxmox_servers.yml new file mode 100644 index 0000000..2d4b45e --- /dev/null +++ b/ansible/group_vars/proxmox_servers.yml @@ -0,0 +1,7 @@ +--- +ansible_user: root +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_enabled: false diff --git a/ansible/group_vars/vntx_servers/main.yml b/ansible/group_vars/vntx_servers/main.yml new file mode 100644 index 0000000..76ca1bf --- /dev/null +++ b/ansible/group_vars/vntx_servers/main.yml @@ -0,0 +1,8 @@ +--- +firewall_trusted_subnets: + - subnet: 100.64.0.0/10 + comment: Tailscale + - subnet: 204.110.188.0/22 + comment: VNTX + - subnet: 10.0.0.0/8 + comment: Internal diff --git a/ansible/host_vars/aprs.w5isp.com.yml b/ansible/host_vars/aprs.w5isp.com.yml new file mode 100644 index 0000000..1fd0cef --- /dev/null +++ b/ansible/host_vars/aprs.w5isp.com.yml @@ -0,0 +1,37 @@ +--- +ansible_user: graham +ansible_host: 204.110.191.232 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 10152 + proto: tcp + comment: APRS-IS full feed + - port: 10152 + proto: udp + comment: APRS-IS full feed UDP + - port: 14580 + proto: tcp + comment: APRS-IS client + - port: 14580 + proto: udp + comment: APRS-IS client UDP + - port: 8080 + proto: udp + comment: APRS-IS UDP submit + - port: 8080 + proto: tcp + comment: APRS-IS HTTP upload + - port: 14501 + proto: tcp + comment: APRS-IS HTTP status + +aprsc_server_id: T2DALLAS +aprsc_passcode: "28302" +aprsc_admin: "Graham McIntire, W5ISP" +aprsc_email: graham@mcintire.me +aprsc_bind_address: 204.110.191.232 +aprsc_uplink: hub-rotate.aprs2.net +aprsc_uplink_port: 20152 diff --git a/ansible/host_vars/db.aprs.me.yml b/ansible/host_vars/db.aprs.me.yml new file mode 100644 index 0000000..f03dbbb --- /dev/null +++ b/ansible/host_vars/db.aprs.me.yml @@ -0,0 +1,142 @@ +--- +ansible_user: graham +ansible_host: 10.0.15.22 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_enabled: false + +# Override role defaults - LXC container with default Debian paths +postgresql_version: "17" +postgresql_storage_setup_enabled: false +postgresql_data_directory: /var/lib/postgresql/17/main +postgresql_wal_directory: /var/lib/postgresql/17/main/pg_wal +postgresql_config_directory: /etc/postgresql/17/main +postgresql_backup_enabled: false + +# PostgreSQL base config (postgresql.conf) +postgresql_config: + # Connection + listen_addresses: "'*'" + max_connections: 100 + port: 5432 + + # Memory (tuned for 8GB RAM, Intel N100 2 vCPU) + shared_buffers: 2GB + effective_cache_size: 6GB + work_mem: 16MB + maintenance_work_mem: 512MB + huge_pages: try + + # NVMe SSD tuning + random_page_cost: 1.1 + effective_io_concurrency: 200 + maintenance_io_concurrency: 200 + + # WAL & Checkpoints (write-heavy APRS workload) + wal_buffers: 64MB + max_wal_size: 2GB + min_wal_size: 512MB + wal_compression: zstd + checkpoint_completion_target: 0.9 + checkpoint_timeout: 10min + synchronous_commit: off + + # Parallelism (constrained by 2 vCPUs) + max_worker_processes: 4 + max_parallel_workers: 2 + max_parallel_workers_per_gather: 1 + max_parallel_maintenance_workers: 1 + + # Autovacuum (aggressive for high-churn packets table) + autovacuum_vacuum_scale_factor: 0.05 + autovacuum_analyze_scale_factor: 0.02 + autovacuum_vacuum_cost_limit: 1000 + autovacuum_naptime: 30s + + # Query planner + default_statistics_target: 200 + + # Monitoring & Logging + shared_preload_libraries: "'pg_stat_statements'" + pg_stat_statements.track: top + track_io_timing: on + log_min_duration_statement: 500 + log_lock_waits: on + log_temp_files: 0 + log_autovacuum_min_duration: 250 + log_line_prefix: "'%m [%p] %q%u@%d '" + log_timezone: "'Etc/UTC'" + cluster_name: "'17/main'" + + # Locale + datestyle: "'iso, mdy'" + timezone: "'Etc/UTC'" + lc_messages: "'C'" + lc_monetary: "'C'" + lc_numeric: "'C'" + lc_time: "'C'" + default_text_search_config: "'pg_catalog.english'" + + # SSL (snakeoil certs) + ssl: on + ssl_cert_file: "'/etc/ssl/certs/ssl-cert-snakeoil.pem'" + ssl_key_file: "'/etc/ssl/private/ssl-cert-snakeoil.key'" + +# pg_hba.conf entries +postgresql_hba_entries: + - type: local + database: all + user: postgres + auth_method: peer + - type: local + database: all + user: all + auth_method: md5 + - type: host + database: all + user: all + address: 127.0.0.1/32 + auth_method: scram-sha-256 + - type: host + database: all + user: all + address: 0.0.0.0/0 + auth_method: md5 + - type: host + database: all + user: all + address: "::1/128" + auth_method: scram-sha-256 + - type: local + database: replication + user: all + auth_method: peer + - type: host + database: replication + user: all + address: 127.0.0.1/32 + auth_method: scram-sha-256 + - type: host + database: replication + user: all + address: "::1/128" + auth_method: scram-sha-256 + +# Database +postgresql_databases: + - name: aprsme_prod + owner: aprs + +postgresql_users: + - name: aprs + password: "{{ vault_postgresql_aprs_password | default(omit) }}" + role_attr_flags: CREATEDB + +postgresql_extensions: + aprsme_prod: + - citext + - pg_stat_statements + - pg_trgm + - postgis diff --git a/ansible/host_vars/db.mcintire.me.yml b/ansible/host_vars/db.mcintire.me.yml new file mode 100644 index 0000000..0e28820 --- /dev/null +++ b/ansible/host_vars/db.mcintire.me.yml @@ -0,0 +1,150 @@ +--- +ansible_user: graham +ansible_host: 10.0.15.20 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_enabled: false + +# Override role defaults - this server uses default Debian paths, not RAID +postgresql_version: "17" +postgresql_storage_setup_enabled: false +postgresql_data_directory: /var/lib/postgresql/17/main +postgresql_wal_directory: /var/lib/postgresql/17/main/pg_wal +postgresql_config_directory: /etc/postgresql/17/main +postgresql_backup_enabled: false + +# TimescaleDB +timescaledb_enabled: true +timescaledb_max_background_workers: 16 +timescaledb_apt_repo: "deb https://packagecloud.io/timescale/timescaledb/debian/ trixie main" + +# PostgreSQL config (matches live server tuning) +postgresql_config: + # Connection + listen_addresses: "'*'" + max_connections: 100 + port: 5432 + + # Memory (tuned for 8GB ARM server) + shared_buffers: 2014MB + work_mem: 5157kB + maintenance_work_mem: 1031576kB + effective_cache_size: 6044MB + + # Disk + random_page_cost: 1.1 + effective_io_concurrency: 256 + + # Workers + max_worker_processes: 23 + max_parallel_workers: 4 + max_parallel_workers_per_gather: 2 + + # WAL + wal_buffers: 16MB + max_wal_size: 1GB + min_wal_size: 512MB + checkpoint_completion_target: 0.9 + + # Autovacuum + autovacuum_max_workers: 10 + autovacuum_naptime: 10 + + # Query tuning + default_statistics_target: 100 + jit: off + + # Compression and locking + default_toast_compression: lz4 + max_locks_per_transaction: 128 + + # Logging + log_line_prefix: "'%m [%p] %q%u@%d '" + log_timezone: "'UTC'" + cluster_name: "'17/main'" + + # Locale + datestyle: "'iso, dmy'" + timezone: "'Europe/London'" + lc_messages: "'en_GB.UTF-8'" + lc_monetary: "'en_GB.UTF-8'" + lc_numeric: "'en_GB.UTF-8'" + lc_time: "'en_GB.UTF-8'" + default_text_search_config: "'pg_catalog.english'" + + # Extensions + shared_preload_libraries: "'pg_stat_statements,timescaledb'" + + # TimescaleDB + timescaledb.max_background_workers: 16 + + # SSL + ssl: on + ssl_cert_file: "'server.crt'" + ssl_key_file: "'server.key'" + +# pg_hba.conf entries (matches live server) +postgresql_hba_entries: + - type: local + database: all + user: postgres + auth_method: peer + - type: local + database: all + user: all + auth_method: peer + - type: host + database: all + user: all + address: 127.0.0.1/32 + auth_method: scram-sha-256 + - type: host + database: all + user: all + address: "::1/128" + auth_method: scram-sha-256 + - type: local + database: replication + user: all + auth_method: peer + - type: host + database: replication + user: all + address: 127.0.0.1/32 + auth_method: scram-sha-256 + - type: host + database: replication + user: all + address: "::1/128" + auth_method: scram-sha-256 + - type: hostssl + database: all + user: all + address: 10.0.8.0/21 + auth_method: scram-sha-256 + +# Databases and users (matches live server) +postgresql_databases: + - name: aprsme_prod + owner: aprsme + - name: gridmap + owner: gridmap + - name: towerops + owner: towerops + +postgresql_users: + - name: aprsme + password: "{{ vault_postgresql_aprsme_password | default(omit) }}" + - name: gridmap + password: "{{ vault_postgresql_gridmap_password | default(omit) }}" + - name: towerops + password: "{{ vault_postgresql_towerops_password | default(omit) }}" + +postgresql_extensions: + aprsme_prod: + - citext + - pg_stat_statements + - pg_trgm + - postgis diff --git a/ansible/host_vars/dokku.w5isp.com.yml b/ansible/host_vars/dokku.w5isp.com.yml index dedd960..80e258b 100644 --- a/ansible/host_vars/dokku.w5isp.com.yml +++ b/ansible/host_vars/dokku.w5isp.com.yml @@ -4,3 +4,11 @@ ansible_host: 204.110.191.218 ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/ansible ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS diff --git a/ansible/host_vars/vm1.w5isp.com.yml b/ansible/host_vars/git.mcintire.me.yml similarity index 85% rename from ansible/host_vars/vm1.w5isp.com.yml rename to ansible/host_vars/git.mcintire.me.yml index 0b404f9..2aca7cf 100644 --- a/ansible/host_vars/vm1.w5isp.com.yml +++ b/ansible/host_vars/git.mcintire.me.yml @@ -6,6 +6,23 @@ ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' public_ip: 172.245.56.83 dns_backend: bind9 +firewall_allow_rules: + - port: 53 + proto: tcp + comment: DNS + - port: 53 + proto: udp + comment: DNS + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS + - port: 2222 + proto: tcp + comment: Forgejo SSH + # Forgejo runner forgejo_runner_token: "I7rqgwumFl3BjuXnaEfroto7RHxGhsIUzM0lgMmM" forgejo_runner_name: "vm1-runner" diff --git a/ansible/host_vars/librenms.vntx.net.vault.yml b/ansible/host_vars/librenms.vntx.net.vault.yml new file mode 100644 index 0000000..ba8d8d3 --- /dev/null +++ b/ansible/host_vars/librenms.vntx.net.vault.yml @@ -0,0 +1,3 @@ +--- +vault_librenms_db_password: tqEoiUsDyEMBe +vault_librenms_app_key: "base64:2Ro8xSViclOmQjFoXgm13h5Mx6WaLlihe/Q7O9qbReA=" diff --git a/ansible/host_vars/librenms.vntx.net.yml b/ansible/host_vars/librenms.vntx.net.yml new file mode 100644 index 0000000..1e11aa2 --- /dev/null +++ b/ansible/host_vars/librenms.vntx.net.yml @@ -0,0 +1,17 @@ +--- +ansible_user: graham +ansible_host: 204.110.191.227 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS + - port: 161 + proto: udp + comment: SNMP diff --git a/ansible/host_vars/logs.vntx.net.yml b/ansible/host_vars/logs.vntx.net.yml new file mode 100644 index 0000000..e4bbf51 --- /dev/null +++ b/ansible/host_vars/logs.vntx.net.yml @@ -0,0 +1,6 @@ +--- +ansible_user: ansible +ansible_host: 204.110.191.229 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/ansible +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' diff --git a/ansible/host_vars/mail.mcintire.me.yml b/ansible/host_vars/mail.mcintire.me.yml index 8aa0679..7bda07a 100644 --- a/ansible/host_vars/mail.mcintire.me.yml +++ b/ansible/host_vars/mail.mcintire.me.yml @@ -1,4 +1,27 @@ --- # Host-specific variables for mail.mcintire.me ansible_user: ansible -ansible_python_interpreter: /usr/bin/python3 \ No newline at end of file +ansible_python_interpreter: /usr/bin/python3 + +firewall_allow_rules: + - port: 25 + proto: tcp + comment: SMTP + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS + - port: 465 + proto: tcp + comment: SMTPS + - port: 587 + proto: tcp + comment: SMTP Submission + - port: 993 + proto: tcp + comment: IMAPS + - port: 995 + proto: tcp + comment: POP3S \ No newline at end of file diff --git a/ansible/host_vars/monitor.vntx.net.yml b/ansible/host_vars/monitor.vntx.net.yml index 6cc66eb..5fc6964 100644 --- a/ansible/host_vars/monitor.vntx.net.yml +++ b/ansible/host_vars/monitor.vntx.net.yml @@ -4,3 +4,11 @@ ansible_host: 204.110.191.241 ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS diff --git a/ansible/host_vars/netbox.vntx.net.yml b/ansible/host_vars/netbox.vntx.net.yml index d91a10e..72f4361 100644 --- a/ansible/host_vars/netbox.vntx.net.yml +++ b/ansible/host_vars/netbox.vntx.net.yml @@ -8,6 +8,14 @@ ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS + # Override defaults for NetBox installation netbox_port: 8001 netbox_secret_key: "{{ vault_netbox_secret_key }}" diff --git a/ansible/host_vars/ns1.as393837.net.yml b/ansible/host_vars/ns1.as393837.net.yml index 6bbbdd7..54c6120 100644 --- a/ansible/host_vars/ns1.as393837.net.yml +++ b/ansible/host_vars/ns1.as393837.net.yml @@ -5,3 +5,11 @@ ansible_python_interpreter: /usr/bin/python3 ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' public_ip: 204.110.191.222 dns_backend: bind9 + +firewall_allow_rules: + - port: 53 + proto: tcp + comment: DNS + - port: 53 + proto: udp + comment: DNS diff --git a/ansible/host_vars/radius.vntx.net.yml b/ansible/host_vars/radius.vntx.net.yml index fdf83b7..14f16d3 100644 --- a/ansible/host_vars/radius.vntx.net.yml +++ b/ansible/host_vars/radius.vntx.net.yml @@ -4,3 +4,17 @@ ansible_host: 204.110.191.248 ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 1812 + proto: udp + comment: RADIUS Auth + - port: 1812 + proto: tcp + comment: RADIUS Auth + - port: 1813 + proto: udp + comment: RADIUS Accounting + - port: 1813 + proto: tcp + comment: RADIUS Accounting diff --git a/ansible/host_vars/resolver1.vntx.net.yml b/ansible/host_vars/resolver1.vntx.net.yml new file mode 100644 index 0000000..4a4ed77 --- /dev/null +++ b/ansible/host_vars/resolver1.vntx.net.yml @@ -0,0 +1,6 @@ +--- +ansible_user: graham +ansible_host: 204.110.191.240 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' diff --git a/ansible/host_vars/resolver2.vntx.net.yml b/ansible/host_vars/resolver2.vntx.net.yml new file mode 100644 index 0000000..5aa0a68 --- /dev/null +++ b/ansible/host_vars/resolver2.vntx.net.yml @@ -0,0 +1,6 @@ +--- +ansible_user: graham +ansible_host: 204.110.191.250 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' diff --git a/ansible/host_vars/skippy.w5isp.com.yml b/ansible/host_vars/skippy.w5isp.com.yml index 080b481..e779551 100644 --- a/ansible/host_vars/skippy.w5isp.com.yml +++ b/ansible/host_vars/skippy.w5isp.com.yml @@ -4,3 +4,14 @@ ansible_host: 204.110.191.8 ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/ansible ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +network: + skip: true + +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS diff --git a/ansible/host_vars/staging.towerops.net.yml b/ansible/host_vars/staging.towerops.net.yml new file mode 100644 index 0000000..91d7f96 --- /dev/null +++ b/ansible/host_vars/staging.towerops.net.yml @@ -0,0 +1,16 @@ +--- +ansible_user: graham +ansible_host: 204.110.191.231 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS + +dokku_domain: staging.towerops.net diff --git a/ansible/host_vars/sync.w5isp.com.vault.yml b/ansible/host_vars/sync.w5isp.com.vault.yml deleted file mode 100644 index 02aa47d..0000000 --- a/ansible/host_vars/sync.w5isp.com.vault.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Vault file for sync.w5isp.com - Mailcow secrets - -vault_mailcow_api_key: "drVv1AtEHuJw5C0+MxlKGGOMYiLx7C6dlfcAFlh2wZY=" -vault_mailcow_dbpass: "om30XAgUbHTcPy3uGNzJAp8L+uGvNyoS" -vault_mailcow_dbroot: "jhIXyrY/DOS8wuRPSmCuRR/DqqXzi2Lq" -vault_mailcow_redispass: "9YZCv2mqofPYl7p4ep5DvL2PVBIBhMK/" \ No newline at end of file diff --git a/ansible/host_vars/sync.w5isp.com.yml b/ansible/host_vars/sync.w5isp.com.yml deleted file mode 100644 index 7416b0e..0000000 --- a/ansible/host_vars/sync.w5isp.com.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -# Host-specific variables for sync.w5isp.com -ansible_user: ansible -ansible_python_interpreter: /usr/bin/python3 - -# Mailcow configuration -mailcow_hostname: mail.w5isp.com -mailcow_http_bind: "127.0.0.1" -mailcow_https_bind: "127.0.0.1" -mailcow_api_key: "{{ vault_mailcow_api_key }}" -mailcow_dbpass: "{{ vault_mailcow_dbpass }}" -mailcow_dbroot: "{{ vault_mailcow_dbroot }}" - -# Since we're using Caddy as reverse proxy -mailcow_skip_lets_encrypt: "y" \ No newline at end of file diff --git a/ansible/host_vars/uisp.vntx.net.yml b/ansible/host_vars/uisp.vntx.net.yml new file mode 100644 index 0000000..eed7578 --- /dev/null +++ b/ansible/host_vars/uisp.vntx.net.yml @@ -0,0 +1,23 @@ +--- +ansible_user: graham +ansible_host: 204.110.191.224 +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 +ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_allow_rules: + - port: 80 + proto: tcp + comment: HTTP + - port: 443 + proto: tcp + comment: HTTPS + - port: 81 + proto: tcp + comment: UISP device management + - port: 8089 + proto: tcp + comment: UISP HTTPS alt + - port: 2055 + proto: udp + comment: NetFlow diff --git a/ansible/host_vars/vpn.vntx.net.yml b/ansible/host_vars/vpn.vntx.net.yml index 3218bfb..65d4b08 100644 --- a/ansible/host_vars/vpn.vntx.net.yml +++ b/ansible/host_vars/vpn.vntx.net.yml @@ -4,3 +4,23 @@ ansible_host: 204.110.191.247 ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/ansible ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' + +firewall_trusted_subnets: + - subnet: 100.64.0.0/10 + comment: Tailscale + - subnet: 204.110.188.0/22 + comment: VNTX + - subnet: 10.0.0.0/8 + comment: Internal + +firewall_allow_rules: + - port: 51820 + proto: udp + comment: WireGuard + +firewall_default_routed: allow +firewall_ip_forward: true +firewall_masquerade_rules: + - source: 10.6.0.0/24 + out_interface: eth0 + comment: wireguard-nat-rule diff --git a/ansible/hosts b/ansible/hosts index c902018..6fb858a 100755 --- a/ansible/hosts +++ b/ansible/hosts @@ -1,10 +1,20 @@ [vntx_servers] -#logs.vntx.net +logs.vntx.net radius.vntx.net vpn.vntx.net netbox.vntx.net #ntp.vntx.net unimus.vntx.net +monitor.vntx.net +librenms.vntx.net +uisp.vntx.net + +[librenms_servers] +librenms.vntx.net + +[resolvers] +resolver1.vntx.net +resolver2.vntx.net [monitoring_servers] monitor.vntx.net @@ -13,22 +23,40 @@ monitor.vntx.net skippy.w5isp.com mail.mcintire.me ansible_host=107.174.178.20 dokku.w5isp.com -camper +aprs.w5isp.com +staging.towerops.net + +[dokku_servers] +staging.towerops.net + +[aprsc_servers] +aprs.w5isp.com [caddy_servers] skippy.w5isp.com -#[syncthing_servers] -#sync.w5isp.com - [bind9_servers] ns1.as393837.net ansible_host=204.110.191.222 -vm1.w5isp.com ansible_host=172.245.56.83 +git.mcintire.me ansible_host=172.245.56.83 [dns_servers:children] bind9_servers [forgejo_servers] -vm1.w5isp.com +git.mcintire.me + +[uisp_servers] +uisp.vntx.net + +[proxmox_servers] +node1 ansible_host=10.0.15.101 +node2 ansible_host=10.0.15.102 +node3 ansible_host=10.0.15.103 [postgresql_servers] +db.mcintire.me ansible_host=10.0.15.20 +db.aprs.me ansible_host=10.0.15.22 + +[home_cluster:children] +proxmox_servers +postgresql_servers diff --git a/ansible/playbook.yml b/ansible/playbook.yml index 2ad89f4..b4c8d42 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -15,12 +15,23 @@ community.general.timezone: name: "{{ timezone }}" -# Resolver play disabled - no hosts in resolvers group -#- name: Apply resolver config -# hosts: resolvers -# gather_facts: true -# roles: -# - { role: resolvers, become: true } +- name: Apply firewall configuration + hosts: all + become: true + gather_facts: true + tags: + - firewall + roles: + - firewall + +- name: Apply resolver config + hosts: resolvers + become: true + gather_facts: true + tags: + - resolvers + roles: + - resolvers # NS play disabled - replaced by BIND9 play below #- name: Apply ns config @@ -113,41 +124,6 @@ roles: - caddy -- name: Configure PostgreSQL servers - hosts: postgresql_servers - become: true - gather_facts: true - tags: - - postgresql - - database - - pre_tasks: - - name: Set hostname - hostname: - name: "{{ inventory_hostname }}" - - - name: Update /etc/hosts - lineinfile: - path: /etc/hosts - regexp: '^127\.0\.1\.1' - line: "127.0.1.1 {{ inventory_hostname }}" - state: present - - - name: Update apt cache - apt: - update_cache: yes - cache_valid_time: 3600 - - - name: Install Python dependencies for Ansible - apt: - name: - - python3-apt - - python3-psycopg2 - state: present - - roles: - - raid - - postgresql - name: Provision BIND9 VM on Proxmox hosts: localhost @@ -296,3 +272,39 @@ - forgejo roles: - forgejo + +- name: Configure LibreNMS servers + hosts: librenms_servers + become: yes + gather_facts: true + tags: + - librenms + roles: + - librenms + +- name: Configure aprsc servers + hosts: aprsc_servers + become: yes + gather_facts: true + tags: + - aprsc + roles: + - aprsc + +- name: Configure UISP servers + hosts: uisp_servers + become: yes + gather_facts: true + tags: + - uisp + roles: + - uisp + +- name: Configure Dokku servers + hosts: dokku_servers + become: yes + gather_facts: true + tags: + - dokku + roles: + - dokku diff --git a/ansible/regenerate-ssh-keys.yml b/ansible/regenerate-ssh-keys.yml index 0269e16..9985050 100644 --- a/ansible/regenerate-ssh-keys.yml +++ b/ansible/regenerate-ssh-keys.yml @@ -1,6 +1,6 @@ --- # One-time playbook to regenerate SSH host keys on existing hosts -# Usage: ansible-playbook -l "vm1,pangolin,camper,birdnet" regenerate-ssh-keys.yml +# Usage: ansible-playbook -l "vm1,pangolin,birdnet" regenerate-ssh-keys.yml # # After running this playbook, you'll need to update your local known_hosts: # ssh-keygen -R hostname diff --git a/ansible/roles/aprsc/defaults/main.yml b/ansible/roles/aprsc/defaults/main.yml new file mode 100644 index 0000000..e894f42 --- /dev/null +++ b/ansible/roles/aprsc/defaults/main.yml @@ -0,0 +1,35 @@ +--- +aprsc_server_id: NOCALL +aprsc_passcode: "0" +aprsc_admin: "Server Admin" +aprsc_email: admin@example.com + +aprsc_path: /opt/aprsc +aprsc_user: aprsc + +# Upstream connection +aprsc_uplink: hub-rotate.aprs2.net +aprsc_uplink_port: 20152 +aprsc_uplink_type: full +aprsc_bind_address: "" + +# Listeners +aprsc_fullfeed_port: 10152 +aprsc_client_port: 14580 +aprsc_udpsubmit_port: 8080 +aprsc_http_status_port: 14501 +aprsc_http_upload_port: 8080 + +# Timeouts +aprsc_upstream_timeout: 15s +aprsc_client_timeout: 48h + +# Logging +aprsc_log_rotate_size: 10 +aprsc_log_rotate_count: 5 + +# Resource limits +aprsc_file_limit: 10000 + +# APT repository +aprsc_apt_repo: "deb http://aprsc-dist.he.fi/aprsc/apt {{ ansible_distribution_release }} main" diff --git a/ansible/roles/aprsc/handlers/main.yml b/ansible/roles/aprsc/handlers/main.yml new file mode 100644 index 0000000..efd71ec --- /dev/null +++ b/ansible/roles/aprsc/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart aprsc + systemd: + name: aprsc + state: restarted diff --git a/ansible/roles/aprsc/tasks/main.yml b/ansible/roles/aprsc/tasks/main.yml new file mode 100644 index 0000000..b4135a9 --- /dev/null +++ b/ansible/roles/aprsc/tasks/main.yml @@ -0,0 +1,33 @@ +--- +- name: Add aprsc APT repository + apt_repository: + repo: "{{ aprsc_apt_repo }}" + state: present + filename: aprsc + +- name: Install aprsc + apt: + name: aprsc + state: present + update_cache: true + +- name: Deploy aprsc configuration + template: + src: aprsc.conf.j2 + dest: "{{ aprsc_path }}/etc/aprsc.conf" + owner: root + group: root + mode: '0644' + notify: restart aprsc + +- name: Enable and start aprsc-chroot service + systemd: + name: aprsc-chroot + enabled: true + state: started + +- name: Enable and start aprsc service + systemd: + name: aprsc + state: started + enabled: true diff --git a/ansible/roles/aprsc/templates/aprsc.conf.j2 b/ansible/roles/aprsc/templates/aprsc.conf.j2 new file mode 100644 index 0000000..f65f573 --- /dev/null +++ b/ansible/roles/aprsc/templates/aprsc.conf.j2 @@ -0,0 +1,39 @@ +# Configuration for aprsc, an APRS-IS server for core servers +# Managed by Ansible + +ServerId {{ aprsc_server_id }} +PassCode {{ aprsc_passcode }} +MyAdmin "{{ aprsc_admin }}" +MyEmail {{ aprsc_email }} + +### Directories ######### +RunDir data + +LogRotate {{ aprsc_log_rotate_size }} {{ aprsc_log_rotate_count }} + +### Intervals and timers ######### +UpstreamTimeout {{ aprsc_upstream_timeout }} +ClientTimeout {{ aprsc_client_timeout }} + +### TCP listener ########## +Listen "Full feed" fullfeed tcp :: {{ aprsc_fullfeed_port }} hidden +Listen "" fullfeed udp :: {{ aprsc_fullfeed_port }} hidden + +Listen "Client-Defined Filters" igate tcp :: {{ aprsc_client_port }} +Listen "" igate udp :: {{ aprsc_client_port }} + +Listen "UDP submit" udpsubmit udp :: {{ aprsc_udpsubmit_port }} + +### Uplink configuration ######## +Uplink "Core rotate" {{ aprsc_uplink_type }} tcp {{ aprsc_uplink }} {{ aprsc_uplink_port }} + +{% if aprsc_bind_address is defined and aprsc_bind_address %} +UplinkBind {{ aprsc_bind_address }} +{% endif %} + +### HTTP server ########## +HTTPStatus 0.0.0.0 {{ aprsc_http_status_port }} +HTTPUpload 0.0.0.0 {{ aprsc_http_upload_port }} + +### Environment ############ +FileLimit {{ aprsc_file_limit }} diff --git a/ansible/roles/base/defaults/main.yml b/ansible/roles/base/defaults/main.yml index 875ce07..44dbd77 100644 --- a/ansible/roles/base/defaults/main.yml +++ b/ansible/roles/base/defaults/main.yml @@ -65,14 +65,21 @@ base_packages_by_distribution: AlmaLinux: - bind-utils - lsof - - mosh - nano - net-tools - - mlocate - - fail2ban - pv - selinux-policy - tar - unzip - qemu-guest-agent - python3-libselinux + +# Packages only available on specific AlmaLinux versions +# EL9 has mosh, mlocate, fail2ban via EPEL; EL10 does not yet +base_packages_almalinux_9: + - mosh + - mlocate + - fail2ban + +base_packages_almalinux_10: + - plocate diff --git a/ansible/roles/base/tasks/main.yml b/ansible/roles/base/tasks/main.yml index 98071ab..ca698b1 100644 --- a/ansible/roles/base/tasks/main.yml +++ b/ansible/roles/base/tasks/main.yml @@ -46,3 +46,10 @@ - include_role: name: alpine when: ansible_facts['os_family'] == "Alpine" + +# Firewall configuration +- include_role: + name: firewall + when: firewall_enabled | default(true) + tags: + - firewall diff --git a/ansible/roles/base/tasks/packages.yml b/ansible/roles/base/tasks/packages.yml index fdd71e1..b062e4b 100644 --- a/ansible/roles/base/tasks/packages.yml +++ b/ansible/roles/base/tasks/packages.yml @@ -4,7 +4,9 @@ base_packages_combined: >- {{ (base_common_packages + base_packages_by_os_family.get(ansible_facts['os_family'], []) - + base_packages_by_distribution.get(ansible_facts['distribution'], [])) | unique }} + + base_packages_by_distribution.get(ansible_facts['distribution'], []) + + (base_packages_almalinux_9 if ansible_facts['distribution'] == 'AlmaLinux' and ansible_facts['distribution_major_version'] == '9' else []) + + (base_packages_almalinux_10 if ansible_facts['distribution'] == 'AlmaLinux' and ansible_facts['distribution_major_version'] == '10' else [])) | unique }} tags: - packages - common diff --git a/ansible/roles/dokku/defaults/main.yml b/ansible/roles/dokku/defaults/main.yml new file mode 100644 index 0000000..45cc071 --- /dev/null +++ b/ansible/roles/dokku/defaults/main.yml @@ -0,0 +1,20 @@ +--- +dokku_domain: "{{ inventory_hostname }}" +dokku_apt_suite: bookworm # trixie packages may not exist yet +dokku_ssh_keys: + - name: graham + key: "https://github.com/gmcintire.keys" +dokku_postgres_plugin_url: https://github.com/dokku/dokku-postgres.git +dokku_postgres_image: "timescale/timescaledb" +dokku_postgres_image_version: "latest-pg17" +dokku_redis_plugin_url: https://github.com/dokku/dokku-redis.git +dokku_redis_image: "valkey/valkey" +dokku_redis_image_version: "8" +dokku_apps: + - name: towerops + domains: + - staging.towerops.net + postgres: + service_name: towerops-db + redis: + service_name: towerops-redis diff --git a/ansible/roles/dokku/handlers/main.yml b/ansible/roles/dokku/handlers/main.yml new file mode 100644 index 0000000..bbf917a --- /dev/null +++ b/ansible/roles/dokku/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart dokku nginx + command: dokku nginx:reload diff --git a/ansible/roles/dokku/tasks/main.yml b/ansible/roles/dokku/tasks/main.yml new file mode 100644 index 0000000..3bf2099 --- /dev/null +++ b/ansible/roles/dokku/tasks/main.yml @@ -0,0 +1,321 @@ +--- +# Install Docker (required by Dokku) +- name: Install prerequisites + apt: + name: + - ca-certificates + - curl + - gnupg + - apt-transport-https + state: present + update_cache: true + +- name: Add Docker GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/debian/gpg + dest: /usr/share/keyrings/docker-archive-keyring.asc + mode: '0644' + +- name: Add Docker repository + ansible.builtin.deb822_repository: + name: docker + types: deb + uris: https://download.docker.com/linux/debian + suites: "{{ ansible_distribution_release }}" + components: stable + architectures: "{{ ansible_architecture | replace('x86_64', 'amd64') }}" + signed_by: /usr/share/keyrings/docker-archive-keyring.asc + state: present + +- name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + - docker-buildx-plugin + state: present + update_cache: true + +- name: Ensure Docker service is started and enabled + systemd: + name: docker + state: started + enabled: true + +# Install Dokku +- name: Add Dokku GPG key + ansible.builtin.get_url: + url: https://packagecloud.io/dokku/dokku/gpgkey + dest: /usr/share/keyrings/dokku-archive-keyring.asc + mode: '0644' + +- name: Add Dokku repository + ansible.builtin.deb822_repository: + name: dokku + types: deb + uris: https://packagecloud.io/dokku/dokku/debian + suites: "{{ dokku_apt_suite }}" + components: main + signed_by: /usr/share/keyrings/dokku-archive-keyring.asc + state: present + +- name: Preseed Dokku configuration + ansible.builtin.debconf: + name: dokku + question: "{{ item.question }}" + value: "{{ item.value }}" + vtype: "{{ item.vtype }}" + loop: + - question: dokku/vhost_enable + value: "true" + vtype: boolean + - question: dokku/hostname + value: "{{ dokku_domain }}" + vtype: string + - question: dokku/skip_key_file + value: "true" + vtype: boolean + loop_control: + label: "{{ item.question }}" + +- name: Install Dokku + apt: + name: dokku + state: present + update_cache: true + +- name: Set Dokku global domain + command: dokku domains:set-global {{ dokku_domain }} + register: dokku_domain_result + changed_when: "'Set' in dokku_domain_result.stdout" + +# SSH keys for git push access +- name: Fetch SSH keys + ansible.builtin.uri: + url: "{{ item.key }}" + return_content: true + register: ssh_key_responses + loop: "{{ dokku_ssh_keys }}" + loop_control: + label: "{{ item.name }}" + delegate_to: localhost + become: false + +- name: Check existing Dokku SSH keys + command: dokku ssh-keys:list + register: dokku_existing_keys + changed_when: false + failed_when: false + +- name: Add SSH keys to Dokku + shell: | + echo '{{ item.content.split("\n") | first }}' | dokku ssh-keys:add {{ item.item.name }} + when: "item.item.name not in dokku_existing_keys.stdout" + loop: "{{ ssh_key_responses.results }}" + loop_control: + label: "{{ item.item.name }}" + +# Install dokku-postgres plugin +- name: Check if postgres plugin is installed + command: dokku plugin:list + register: dokku_plugins + changed_when: false + +- name: Install dokku-postgres plugin + command: dokku plugin:install {{ dokku_postgres_plugin_url }} + when: "'postgres' not in dokku_plugins.stdout" + +- name: Install dokku-redis plugin + command: dokku plugin:install {{ dokku_redis_plugin_url }} + when: "'redis' not in dokku_plugins.stdout" + +# Create apps and postgres services +- name: Check existing apps + command: dokku apps:list + register: dokku_apps_list + changed_when: false + +- name: Create Dokku apps + command: "dokku apps:create {{ item.name }}" + when: "item.name not in dokku_apps_list.stdout_lines" + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + +- name: Set app domains + shell: | + current=$(dokku domains:report {{ item.name }} --domains-app-vhosts) + for domain in {{ item.domains | join(' ') }}; do + if ! echo "$current" | grep -q "$domain"; then + dokku domains:add {{ item.name }} "$domain" + fi + done + when: item.domains is defined + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: domains_result + changed_when: "'added' in domains_result.stdout | default('')" + +- name: Check if app has SSL cert + shell: "dokku certs:report {{ item.name }} --ssl-enabled 2>&1" + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: certs_check + changed_when: false + failed_when: false + +- name: Generate self-signed SSL cert for apps + shell: | + mkdir -p /home/dokku/{{ item.item.name }}/tls + openssl req -new -x509 -days 3650 -nodes \ + -out /home/dokku/{{ item.item.name }}/tls/server.crt \ + -keyout /home/dokku/{{ item.item.name }}/tls/server.key \ + -subj "/CN={{ item.item.domains | first }}" + chown -R dokku:dokku /home/dokku/{{ item.item.name }}/tls + chmod 600 /home/dokku/{{ item.item.name }}/tls/server.key + dokku proxy:build-config {{ item.item.name }} + when: + - item.item.domains is defined + - "'true' not in item.stdout" + loop: "{{ certs_check.results }}" + loop_control: + label: "{{ item.item.name }}" + +- name: Check existing postgres services + command: dokku postgres:list + register: dokku_postgres_list + changed_when: false + failed_when: false + +- name: Create postgres services with TimescaleDB + command: > + dokku postgres:create {{ item.postgres.service_name }} + --image {{ dokku_postgres_image }} + --image-version {{ dokku_postgres_image_version }} + when: + - item.postgres is defined + - "item.postgres.service_name not in dokku_postgres_list.stdout" + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: postgres_create_result + +# Handle TimescaleDB SSL cert issue - container may get stuck restarting without certs +- name: Wait for postgres services to stabilize + pause: + seconds: 10 + when: postgres_create_result.changed | default(false) + +- name: Check postgres service status + command: "dokku postgres:info {{ item.postgres.service_name }}" + when: + - item.postgres is defined + - postgres_create_result.changed | default(false) + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: postgres_info + changed_when: false + failed_when: false + +- name: Fix TimescaleDB SSL certs if container is restarting + shell: | + CONTAINER_ID=$(docker ps -a --filter "name=dokku.postgres.{{ item.postgres.service_name }}" --format '{{"{{"}}.ID{{"}}"}}') + if [ -n "$CONTAINER_ID" ]; then + DATA_DIR=$(docker inspect "$CONTAINER_ID" --format '{{"{{" }}range .Mounts{{"}}"}}{{"{{"}}if eq .Destination "/var/lib/postgresql/data"{{"}}"}}{{"{{"}} .Source {{"}}"}}{{"{{"}}end{{"}}"}}{{"{{"}}end{{"}}"}}') + if [ -n "$DATA_DIR" ] && [ ! -f "$DATA_DIR/server.crt" ]; then + openssl req -new -x509 -days 3650 -nodes \ + -out "$DATA_DIR/server.crt" \ + -keyout "$DATA_DIR/server.key" \ + -subj "/CN=dokku-postgres-{{ item.postgres.service_name }}" + chown 999:999 "$DATA_DIR/server.crt" "$DATA_DIR/server.key" + chmod 600 "$DATA_DIR/server.key" + dokku postgres:restart {{ item.postgres.service_name }} + fi + fi + when: + - item.postgres is defined + - postgres_create_result.changed | default(false) + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + changed_when: false + +# Enable TimescaleDB extension +- name: Enable timescaledb extension + shell: | + echo "CREATE EXTENSION IF NOT EXISTS timescaledb;" | dokku postgres:connect {{ item.postgres.service_name }} + when: + - item.postgres is defined + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: timescaledb_ext + changed_when: "'CREATE EXTENSION' in timescaledb_ext.stdout" + failed_when: + - timescaledb_ext.rc != 0 + - "'already exists' not in timescaledb_ext.stderr" + +# Link postgres to apps +- name: Check postgres links + shell: "dokku postgres:linked {{ item.postgres.service_name }} {{ item.name }} && echo linked || echo not_linked" + when: + - item.postgres is defined + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: postgres_link_check + changed_when: false + failed_when: false + +- name: Link postgres to apps + command: "dokku postgres:link {{ item.item.postgres.service_name }} {{ item.item.name }}" + when: + - item.item.postgres is defined + - "'not_linked' in item.stdout" + loop: "{{ postgres_link_check.results }}" + loop_control: + label: "{{ item.item.name }}" + +# Redis/Valkey services +- name: Check existing redis services + command: dokku redis:list + register: dokku_redis_list + changed_when: false + failed_when: false + +- name: Create redis services with Valkey + command: > + dokku redis:create {{ item.redis.service_name }} + --image {{ dokku_redis_image }} + --image-version {{ dokku_redis_image_version }} + when: + - item.redis is defined + - "item.redis.service_name not in dokku_redis_list.stdout" + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + +- name: Check redis links + shell: "dokku redis:linked {{ item.redis.service_name }} {{ item.name }} && echo linked || echo not_linked" + when: + - item.redis is defined + loop: "{{ dokku_apps }}" + loop_control: + label: "{{ item.name }}" + register: redis_link_check + changed_when: false + failed_when: false + +- name: Link redis to apps + command: "dokku redis:link {{ item.item.redis.service_name }} {{ item.item.name }}" + when: + - item.item.redis is defined + - "'not_linked' in item.stdout" + loop: "{{ redis_link_check.results }}" + loop_control: + label: "{{ item.item.name }}" diff --git a/ansible/roles/firewall/defaults/main.yml b/ansible/roles/firewall/defaults/main.yml new file mode 100644 index 0000000..36afef0 --- /dev/null +++ b/ansible/roles/firewall/defaults/main.yml @@ -0,0 +1,38 @@ +--- +# Trusted subnets - SSH and all ports allowed from these +firewall_trusted_subnets: + - subnet: 100.64.0.0/10 + comment: Tailscale + - subnet: 204.110.188.0/22 + comment: VNTX + +# Default policy +firewall_default_incoming: deny +firewall_default_outgoing: allow + +# Per-host rules for publicly exposed services +# Override in host_vars or group_vars as needed +# Example: +# firewall_allow_rules: +# - port: 80 +# proto: tcp +# comment: HTTP +# - port: 443 +# proto: tcp +# comment: HTTPS +firewall_allow_rules: [] + +# Routed/forwarding policy (allow, deny, reject) +# Set to allow on hosts that need to forward traffic (e.g. VPN gateways) +firewall_default_routed: deny + +# Enable IP forwarding (sysctl net.ipv4.ip_forward) +firewall_ip_forward: false + +# NAT masquerade rules for VPN/routing +# Example: +# firewall_masquerade_rules: +# - source: 10.6.0.0/24 +# out_interface: eth0 +# comment: wireguard-nat-rule +firewall_masquerade_rules: [] diff --git a/ansible/roles/firewall/handlers/main.yml b/ansible/roles/firewall/handlers/main.yml new file mode 100644 index 0000000..f49a4f6 --- /dev/null +++ b/ansible/roles/firewall/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: Reload ufw + ufw: + state: reloaded diff --git a/ansible/roles/firewall/tasks/main.yml b/ansible/roles/firewall/tasks/main.yml new file mode 100644 index 0000000..857774d --- /dev/null +++ b/ansible/roles/firewall/tasks/main.yml @@ -0,0 +1,125 @@ +--- +# Firewall configuration using ufw +# Default deny incoming, allow outgoing +# Trusted subnets get full access +# Per-host rules for publicly exposed services + +- name: Install ufw + apt: + name: ufw + state: present + update_cache: true + when: ansible_os_family == "Debian" + +- name: Install firewalld + dnf: + name: firewalld + state: present + when: ansible_os_family == "RedHat" + +- name: Ensure firewalld is started and enabled + systemd: + name: firewalld + state: started + enabled: true + when: ansible_os_family == "RedHat" + +# --- Firewalld (RedHat/AlmaLinux) --- + +- name: Allow trusted subnets in firewalld + ansible.posix.firewalld: + zone: public + source: "{{ item.subnet }}" + permanent: true + immediate: true + state: enabled + loop: "{{ firewall_trusted_subnets }}" + when: ansible_os_family == "RedHat" + +- name: Allow per-host public services in firewalld + ansible.posix.firewalld: + zone: public + port: "{{ item.port | string }}/{{ item.proto }}" + permanent: true + immediate: true + state: enabled + loop: "{{ firewall_allow_rules }}" + when: + - ansible_os_family == "RedHat" + - firewall_allow_rules | length > 0 + +# --- UFW (Debian/Ubuntu) --- + +- name: Reset ufw to defaults + ufw: + state: reset + when: ansible_os_family == "Debian" + +- name: Set default incoming policy to deny + ufw: + direction: incoming + policy: "{{ firewall_default_incoming }}" + when: ansible_os_family == "Debian" + +- name: Set default outgoing policy to allow + ufw: + direction: outgoing + policy: "{{ firewall_default_outgoing }}" + when: ansible_os_family == "Debian" + +- name: Set default routed policy + ufw: + direction: routed + policy: "{{ firewall_default_routed }}" + when: ansible_os_family == "Debian" + +- name: Allow all traffic from trusted subnets + ufw: + rule: allow + from_ip: "{{ item.subnet }}" + comment: "{{ item.comment }}" + loop: "{{ firewall_trusted_subnets }}" + when: ansible_os_family == "Debian" + +- name: Allow per-host public services + ufw: + rule: allow + port: "{{ item.port | string }}" + proto: "{{ item.proto }}" + comment: "{{ item.comment }}" + loop: "{{ firewall_allow_rules }}" + when: + - ansible_os_family == "Debian" + - firewall_allow_rules | length > 0 + +- name: Enable IP forwarding + ansible.posix.sysctl: + name: net.ipv4.ip_forward + value: '1' + sysctl_set: true + state: present + reload: true + when: + - ansible_os_family == "Debian" + - firewall_ip_forward + +- name: Configure NAT masquerade rules in ufw before.rules + ansible.builtin.blockinfile: + path: /etc/ufw/before.rules + insertbefore: "\\*filter" + marker: "# {mark} ANSIBLE MANAGED - {{ item.comment }}" + block: | + *nat + :POSTROUTING ACCEPT [0:0] + -A POSTROUTING -s {{ item.source }} -o {{ item.out_interface }} -j MASQUERADE -m comment --comment "{{ item.comment }}" + COMMIT + loop: "{{ firewall_masquerade_rules }}" + when: + - ansible_os_family == "Debian" + - firewall_masquerade_rules | length > 0 + notify: Reload ufw + +- name: Enable ufw + ufw: + state: enabled + when: ansible_os_family == "Debian" diff --git a/ansible/roles/forgejo/defaults/main.yml b/ansible/roles/forgejo/defaults/main.yml index 8dbb883..79caaad 100644 --- a/ansible/roles/forgejo/defaults/main.yml +++ b/ansible/roles/forgejo/defaults/main.yml @@ -1,6 +1,6 @@ --- forgejo_home: /opt/forgejo -forgejo_version: "10" +forgejo_version: "15.0-test" forgejo_http_port: 3000 forgejo_ssh_port: 2222 forgejo_domain: git.mcintire.me @@ -10,7 +10,7 @@ forgejo_root_url: "https://{{ forgejo_domain }}" forgejo_app_name: "Forgejo" # Runner settings -forgejo_runner_version: "6.2.2" +forgejo_runner_version: "12.7.1" forgejo_runner_token: "" forgejo_runner_name: "default-runner" forgejo_runner_labels: "docker:docker://node:20-bookworm,ubuntu-latest:docker://ubuntu:latest,ubuntu-22.04:docker://ubuntu:22.04" diff --git a/ansible/roles/forgejo/templates/docker-compose.yml.j2 b/ansible/roles/forgejo/templates/docker-compose.yml.j2 index 34bc282..171b90f 100644 --- a/ansible/roles/forgejo/templates/docker-compose.yml.j2 +++ b/ansible/roles/forgejo/templates/docker-compose.yml.j2 @@ -1,7 +1,7 @@ --- services: forgejo: - image: codeberg.org/forgejo/forgejo:{{ forgejo_version }} + image: codeberg.org/forgejo-experimental/forgejo:{{ forgejo_version }} container_name: forgejo restart: unless-stopped environment: diff --git a/ansible/roles/general/tasks/debian/icinga2.yml b/ansible/roles/general/tasks/debian/icinga2.yml index d4f0c23..912b79b 100644 --- a/ansible/roles/general/tasks/debian/icinga2.yml +++ b/ansible/roles/general/tasks/debian/icinga2.yml @@ -94,7 +94,7 @@ - name: zones.conf template: - src: templates/etc/icinga2/zones.conf.j2 + src: etc/icinga2/zones.conf.j2 dest: /etc/icinga2/zones.conf owner: nagios group: nagios @@ -104,7 +104,7 @@ - name: icinga2.conf template: - src: templates/etc/icinga2/icinga2.conf.j2 + src: etc/icinga2/icinga2.conf.j2 dest: /etc/icinga2/icinga2.conf owner: nagios group: nagios @@ -114,7 +114,7 @@ - name: checks.conf template: - src: templates/etc/icinga2/conf.d/checks.conf.j2 + src: etc/icinga2/conf.d/checks.conf.j2 dest: /etc/icinga2/conf.d/checks.conf owner: nagios group: nagios diff --git a/ansible/roles/general/tasks/debian/network.yml b/ansible/roles/general/tasks/debian/network.yml index c0700b2..9989fe6 100644 --- a/ansible/roles/general/tasks/debian/network.yml +++ b/ansible/roles/general/tasks/debian/network.yml @@ -122,9 +122,11 @@ owner: root group: root mode: 00644 + follow: yes vars: domain: "{{ network.domain | default('local') }}" search_domains: "{{ network.search_domains | default([domain]) }}" nameservers: "{{ network.nameservers | default(['9.9.9.9', '149.112.112.112']) }}" + when: network.skip | default(False) != True tags: - network_resolv_conf diff --git a/ansible/roles/general/templates/etc/icinga2/conf.d/checks.conf.j2 b/ansible/roles/general/templates/etc/icinga2/conf.d/checks.conf.j2 new file mode 100644 index 0000000..c64abf2 --- /dev/null +++ b/ansible/roles/general/templates/etc/icinga2/conf.d/checks.conf.j2 @@ -0,0 +1 @@ +// Managed by Ansible diff --git a/ansible/roles/general/templates/etc/icinga2/icinga2.conf.j2 b/ansible/roles/general/templates/etc/icinga2/icinga2.conf.j2 new file mode 100644 index 0000000..0e5debe --- /dev/null +++ b/ansible/roles/general/templates/etc/icinga2/icinga2.conf.j2 @@ -0,0 +1,58 @@ +/** + * Icinga 2 configuration file + * - this is where you define settings for the Icinga application including + * which hosts/services to check. + * + * For an overview of all available configuration options please refer + * to the documentation that is distributed as part of Icinga 2. + */ + +/** + * The constants.conf defines global constants. + */ +include "constants.conf" + +/** + * The zones.conf defines zones for a cluster setup. + * Not required for single instance setups. + */ +include "zones.conf" + +/** + * The Icinga Template Library (ITL) provides a number of useful templates + * and command definitions. + * Common monitoring plugin command definitions are included separately. + */ +include +include +include +include + +/** + * This includes the Icinga 2 Windows plugins. These command definitions + * are required on a master node when a client is used as command endpoint. + */ +include + +/** + * This includes the NSClient++ check commands. These command definitions + * are required on a master node when a client is used as command endpoint. + */ +include + +/** + * The features-available directory contains a number of configuration + * files for features which can be enabled and disabled using the + * icinga2 feature enable / icinga2 feature disable CLI commands. + * These commands work by creating and removing symbolic links in + * the features-enabled directory. + */ +include "features-enabled/*.conf" + +/** + * Although in theory you could define all your objects in this file + * the preferred way is to create separate directories and files in the conf.d + * directory. Each of these files must have the file extension ".conf". + */ +// Disabled by the node setup CLI command +// include_recursive "conf.d" diff --git a/ansible/roles/general/templates/etc/icinga2/zones.conf.j2 b/ansible/roles/general/templates/etc/icinga2/zones.conf.j2 new file mode 100644 index 0000000..d55c5ac --- /dev/null +++ b/ansible/roles/general/templates/etc/icinga2/zones.conf.j2 @@ -0,0 +1,14 @@ +object Endpoint "{{ inventory_hostname }}" { +} + +object Zone "master" { + endpoints = [ "{{ inventory_hostname }}" ] +} + +object Zone "global-templates" { + global = true +} + +object Zone "director-global" { + global = true +} diff --git a/ansible/roles/librenms/defaults/main.yml b/ansible/roles/librenms/defaults/main.yml new file mode 100644 index 0000000..557885d --- /dev/null +++ b/ansible/roles/librenms/defaults/main.yml @@ -0,0 +1,24 @@ +--- +librenms_user: librenms +librenms_group: librenms +librenms_path: /opt/librenms +librenms_domain: librenms.vntx.net + +# PHP +librenms_php_version: "8.4" +librenms_php_fpm_socket: /run/php-fpm-librenms.sock + +# PHP-FPM pool settings +librenms_fpm_max_children: 5 +librenms_fpm_start_servers: 2 +librenms_fpm_min_spare_servers: 1 +librenms_fpm_max_spare_servers: 3 + +# MariaDB +librenms_db_name: librenms +librenms_db_user: librenms +librenms_db_password: "{{ vault_librenms_db_password }}" +librenms_app_key: "{{ vault_librenms_app_key }}" + +# Poller +librenms_poller_threads: 16 diff --git a/ansible/roles/librenms/handlers/main.yml b/ansible/roles/librenms/handlers/main.yml new file mode 100644 index 0000000..004e613 --- /dev/null +++ b/ansible/roles/librenms/handlers/main.yml @@ -0,0 +1,29 @@ +--- +- name: restart php-fpm + service: + name: "php{{ librenms_php_version }}-fpm" + state: restarted + +- name: restart caddy + service: + name: caddy + state: restarted + +- name: restart snmpd + service: + name: snmpd + state: restarted + +- name: restart postfix + service: + name: postfix + state: restarted + +- name: reload systemd + systemd: + daemon_reload: true + +- name: restart librenms-scheduler + systemd: + name: librenms-scheduler.timer + state: restarted diff --git a/ansible/roles/librenms/tasks/main.yml b/ansible/roles/librenms/tasks/main.yml new file mode 100644 index 0000000..759ac5e --- /dev/null +++ b/ansible/roles/librenms/tasks/main.yml @@ -0,0 +1,241 @@ +--- +- name: Install MariaDB APT repository + ansible.builtin.deb822_repository: + name: mariadb + types: deb + uris: https://mirror.rackspace.com/mariadb/repo/12.2/debian + suites: trixie + components: main + signed_by: https://mariadb.org/mariadb_release_signing_key.pgp + state: present + +- name: Install PHP APT repository (sury) + ansible.builtin.deb822_repository: + name: php + types: deb + uris: https://packages.sury.org/php/ + suites: trixie + components: main + signed_by: https://packages.sury.org/php/apt.gpg + state: present + +- name: Install required packages + apt: + name: + - acl + - curl + - fping + - git + - graphviz + - imagemagick + - mariadb-client + - mariadb-server + - mtr-tiny + - nmap + - python3-pymysql + - rrdtool + - snmp + - snmpd + - unzip + - whois + - "php{{ librenms_php_version }}-bcmath" + - "php{{ librenms_php_version }}-cli" + - "php{{ librenms_php_version }}-curl" + - "php{{ librenms_php_version }}-fpm" + - "php{{ librenms_php_version }}-gd" + - "php{{ librenms_php_version }}-gmp" + - "php{{ librenms_php_version }}-intl" + - "php{{ librenms_php_version }}-mbstring" + - "php{{ librenms_php_version }}-mysql" + - "php{{ librenms_php_version }}-snmp" + - "php{{ librenms_php_version }}-xml" + - "php{{ librenms_php_version }}-zip" + - postfix + state: present + update_cache: true + +- name: Create librenms group + group: + name: "{{ librenms_group }}" + system: true + state: present + +- name: Create librenms user + user: + name: "{{ librenms_user }}" + group: "{{ librenms_group }}" + home: "{{ librenms_path }}" + shell: /usr/bin/bash + system: true + create_home: false + state: present + +- name: Check if LibreNMS is already installed + stat: + path: "{{ librenms_path }}/lnms" + register: librenms_installed + +- name: Clone LibreNMS repository + git: + repo: https://github.com/librenms/librenms.git + dest: "{{ librenms_path }}" + version: master + depth: 1 + become_user: "{{ librenms_user }}" + when: not librenms_installed.stat.exists + +- name: Set LibreNMS directory ownership + file: + path: "{{ librenms_path }}" + owner: "{{ librenms_user }}" + group: "{{ librenms_group }}" + recurse: true + changed_when: false + +- name: Set LibreNMS directory permissions + file: + path: "{{ librenms_path }}" + mode: '0771' + state: directory + +- name: Ensure writable directories exist + file: + path: "{{ librenms_path }}/{{ item }}" + owner: "{{ librenms_user }}" + group: "{{ librenms_group }}" + mode: '0775' + state: directory + loop: + - rrd + - storage + - storage/logs + - bootstrap/cache + +- name: Set ACLs for LibreNMS directories + acl: + path: "{{ librenms_path }}/{{ item }}" + entry: "default:group::rwx" + state: present + loop: + - rrd + - storage + - storage/logs + - bootstrap/cache + +- name: Deploy LibreNMS .env file + template: + src: env.j2 + dest: "{{ librenms_path }}/.env" + owner: "{{ librenms_user }}" + group: "{{ librenms_group }}" + mode: '0640' + +- name: Configure PHP-FPM pool for LibreNMS + template: + src: php-fpm-librenms.conf.j2 + dest: "/etc/php/{{ librenms_php_version }}/fpm/pool.d/librenms.conf" + owner: root + group: root + mode: '0644' + notify: restart php-fpm + +- name: Set PHP memory_limit + lineinfile: + path: "/etc/php/{{ librenms_php_version }}/fpm/php.ini" + regexp: '^memory_limit\s*=' + line: 'memory_limit = 512M' + notify: restart php-fpm + +- name: Configure Caddy for LibreNMS + template: + src: Caddyfile.j2 + dest: /etc/caddy/Caddyfile + owner: root + group: root + mode: '0644' + notify: restart caddy + +- name: Deploy LibreNMS cron jobs + template: + src: cron-librenms.j2 + dest: /etc/cron.d/librenms + owner: root + group: root + mode: '0644' + +- name: Deploy LibreNMS scheduler service + template: + src: librenms-scheduler.service.j2 + dest: /etc/systemd/system/librenms-scheduler.service + owner: root + group: root + mode: '0644' + notify: reload systemd + +- name: Deploy LibreNMS scheduler timer + template: + src: librenms-scheduler.timer.j2 + dest: /etc/systemd/system/librenms-scheduler.timer + owner: root + group: root + mode: '0644' + notify: + - reload systemd + - restart librenms-scheduler + +- name: Enable and start LibreNMS scheduler timer + systemd: + name: librenms-scheduler.timer + state: started + enabled: true + +- name: Configure SNMPD + template: + src: snmpd.conf.j2 + dest: /etc/snmp/snmpd.conf + owner: root + group: root + mode: '0600' + notify: restart snmpd + +- name: Configure Postfix for local-only delivery + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + loop: + - regexp: '^inet_interfaces\s*=' + line: 'inet_interfaces = loopback-only' + - regexp: '^mydestination\s*=' + line: 'mydestination = $myhostname, localhost.$mydomain, localhost' + notify: restart postfix + +- name: Ensure MariaDB is running + service: + name: mariadb + state: started + enabled: true + +- name: Ensure Caddy is running + service: + name: caddy + state: started + enabled: true + +- name: Ensure PHP-FPM is running + service: + name: "php{{ librenms_php_version }}-fpm" + state: started + enabled: true + +- name: Ensure SNMPD is running + service: + name: snmpd + state: started + enabled: true + +- name: Ensure Postfix is running + service: + name: postfix + state: started + enabled: true diff --git a/ansible/roles/librenms/templates/Caddyfile.j2 b/ansible/roles/librenms/templates/Caddyfile.j2 new file mode 100644 index 0000000..dc77238 --- /dev/null +++ b/ansible/roles/librenms/templates/Caddyfile.j2 @@ -0,0 +1,16 @@ +{{ librenms_domain }} { + root * {{ librenms_path }}/html + + encode gzip + + php_fastcgi unix/{{ librenms_php_fpm_socket }} + + file_server + + @dotfiles path */.* + @wellknown path /.well-known/* + handle @wellknown { + file_server + } + respond @dotfiles 403 +} diff --git a/ansible/roles/librenms/templates/cron-librenms.j2 b/ansible/roles/librenms/templates/cron-librenms.j2 new file mode 100644 index 0000000..6f06549 --- /dev/null +++ b/ansible/roles/librenms/templates/cron-librenms.j2 @@ -0,0 +1,12 @@ +33 */6 * * * {{ librenms_user }} {{ librenms_path }}/cronic {{ librenms_path }}/discovery-wrapper.py 1 +*/5 * * * * {{ librenms_user }} {{ librenms_path }}/discovery.php -h new >> /dev/null 2>&1 + +*/5 * * * * {{ librenms_user }} {{ librenms_path }}/cronic {{ librenms_path }}/poller-wrapper.py {{ librenms_poller_threads }} +* * * * * {{ librenms_user }} {{ librenms_path }}/alerts.php >> /dev/null 2>&1 + +*/5 * * * * {{ librenms_user }} {{ librenms_path }}/poll-billing.php >> /dev/null 2>&1 +01 * * * * {{ librenms_user }} {{ librenms_path }}/billing-calculate.php >> /dev/null 2>&1 +*/5 * * * * {{ librenms_user }} {{ librenms_path }}/check-services.php >> /dev/null 2>&1 + +# Daily maintenance script. DO NOT DISABLE! +19 0 * * * {{ librenms_user }} {{ librenms_path }}/daily.sh >> /dev/null 2>&1 diff --git a/ansible/roles/librenms/templates/env.j2 b/ansible/roles/librenms/templates/env.j2 new file mode 100644 index 0000000..ef8a6f6 --- /dev/null +++ b/ansible/roles/librenms/templates/env.j2 @@ -0,0 +1,5 @@ +DB_DATABASE={{ librenms_db_name }} +DB_USERNAME={{ librenms_db_user }} +DB_PASSWORD={{ librenms_db_password }} +APP_KEY={{ librenms_app_key }} +NODE_ID={{ librenms_node_id | default('695bd7d80d63b') }} diff --git a/ansible/roles/librenms/templates/librenms-scheduler.service.j2 b/ansible/roles/librenms/templates/librenms-scheduler.service.j2 new file mode 100644 index 0000000..4edcc3a --- /dev/null +++ b/ansible/roles/librenms/templates/librenms-scheduler.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=LibreNMS Scheduler + +[Service] +Type=oneshot +StandardOutput=null +StandardError=null +WorkingDirectory={{ librenms_path }}/ +ExecStart=/usr/bin/env php artisan schedule:run --no-ansi --no-interaction +User={{ librenms_user }} +Group={{ librenms_group }} diff --git a/ansible/roles/librenms/templates/librenms-scheduler.timer.j2 b/ansible/roles/librenms/templates/librenms-scheduler.timer.j2 new file mode 100644 index 0000000..8a02b68 --- /dev/null +++ b/ansible/roles/librenms/templates/librenms-scheduler.timer.j2 @@ -0,0 +1,9 @@ +[Unit] +Description=LibreNMS Scheduler + +[Timer] +OnCalendar=minutely +AccuracySec=1second + +[Install] +WantedBy=timers.target diff --git a/ansible/roles/librenms/templates/php-fpm-librenms.conf.j2 b/ansible/roles/librenms/templates/php-fpm-librenms.conf.j2 new file mode 100644 index 0000000..a3ae6e5 --- /dev/null +++ b/ansible/roles/librenms/templates/php-fpm-librenms.conf.j2 @@ -0,0 +1,14 @@ +[librenms] +user = {{ librenms_user }} +group = {{ librenms_group }} + +listen = {{ librenms_php_fpm_socket }} + +listen.owner = www-data +listen.group = {{ librenms_group }} + +pm = dynamic +pm.max_children = {{ librenms_fpm_max_children }} +pm.start_servers = {{ librenms_fpm_start_servers }} +pm.min_spare_servers = {{ librenms_fpm_min_spare_servers }} +pm.max_spare_servers = {{ librenms_fpm_max_spare_servers }} diff --git a/ansible/roles/librenms/templates/snmpd.conf.j2 b/ansible/roles/librenms/templates/snmpd.conf.j2 new file mode 100644 index 0000000..89495be --- /dev/null +++ b/ansible/roles/librenms/templates/snmpd.conf.j2 @@ -0,0 +1,3 @@ +rocommunity public 127.0.0.1 +syslocation "VNTX Data Center" +syscontact admin@vntx.net diff --git a/ansible/roles/postgresql/defaults/main.yml b/ansible/roles/postgresql/defaults/main.yml deleted file mode 100644 index 91a586b..0000000 --- a/ansible/roles/postgresql/defaults/main.yml +++ /dev/null @@ -1,122 +0,0 @@ ---- -# PostgreSQL version -postgresql_version: 17 - -# Password management -use_1password: false # Set to true to use 1Password for password storage -op_account_id: "YOOATCZZSVGH7AD6VABUVPORLI" # Default 1Password account ID - -# Storage configuration -postgresql_storage_setup_enabled: false # Set to true to configure storage -postgresql_storage_type: raid0 # Options: single_nvme, raid0 -postgresql_raid_device: /dev/md0 -postgresql_storage_device: "{{ postgresql_raid_device if postgresql_storage_type == 'raid0' else '/dev/nvme0n1p1' }}" -postgresql_mount_point: /mnt/pgdata - -# PostgreSQL configuration -postgresql_data_directory: /mnt/pgdata/{{ postgresql_version }}/main -postgresql_wal_directory: /mnt/pgdata/wal -postgresql_archive_directory: /mnt/pgdata/archive -postgresql_config_directory: /etc/postgresql/{{ postgresql_version }}/main - -# Database settings for APRS.me -postgresql_databases: - - name: aprsme_prod - owner: aprsme - -postgresql_users: - - name: aprsme - password: "{{ vault_postgresql_aprsme_password }}" # Store in vault - priv: "aprsme_prod:ALL" - -# Performance tuning for CM3588 with 16GB RAM -# These are optimized for APRS.me workload -postgresql_config: - # Connection settings - listen_addresses: "'*'" - max_connections: 200 # Increased for 16GB system - - # Memory settings (for 16GB RAM system) - shared_buffers: 4GB # 25% of RAM - effective_cache_size: 12GB # 75% of RAM - maintenance_work_mem: 1GB - work_mem: 64MB # Increased for available memory - - # Disk settings optimized for RAID0 SSDs - random_page_cost: 1.1 # SSD optimization - effective_io_concurrency: 200 # Modern SSD - max_worker_processes: 4 # Pi 5 has 4 cores - max_parallel_workers_per_gather: 2 - max_parallel_workers: 4 - max_parallel_maintenance_workers: 2 - - # Write performance settings - wal_buffers: 32MB - checkpoint_completion_target: 0.9 - checkpoint_timeout: 10min - max_wal_size: 4GB - min_wal_size: 512MB - - # Logging - log_min_duration_statement: 1000 # Log queries over 1 second - log_checkpoints: on - log_connections: on - log_disconnections: on - log_line_prefix: "'%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '" - log_timezone: 'UTC' - - # Query tuning - default_statistics_target: 100 - - # Autovacuum tuning for high insert rate - autovacuum: on - autovacuum_max_workers: 4 - autovacuum_naptime: 30s # More frequent checks - autovacuum_vacuum_threshold: 50 - autovacuum_analyze_threshold: 50 - autovacuum_vacuum_scale_factor: 0.1 # 10% for more frequent vacuums - autovacuum_analyze_scale_factor: 0.05 # 5% for more frequent analyzes - - # Enable query planning statistics - track_activity_query_size: 2048 - track_io_timing: on - track_functions: all - - # PostGIS specific - postgis.gdal_datapath: "'/usr/share/gdal'" - -# Security settings -postgresql_hba_entries: - - type: local - database: all - user: postgres - auth_method: peer - - type: local - database: all - user: all - auth_method: peer - - type: host - database: all - user: all - address: 127.0.0.1/32 - auth_method: scram-sha-256 - - type: host - database: all - user: all - address: ::1/128 - auth_method: scram-sha-256 - - type: hostssl - database: aprsme_prod - user: aprsme - address: 10.0.0.0/8 # Adjust for your network - auth_method: scram-sha-256 - -# Backup settings -postgresql_backup_enabled: true -postgresql_backup_directory: /mnt/pgdata/backups -postgresql_backup_schedule: - minute: "0" - hour: "2" - day: "*" - month: "*" - weekday: "*" \ No newline at end of file diff --git a/ansible/roles/postgresql/handlers/main.yml b/ansible/roles/postgresql/handlers/main.yml deleted file mode 100644 index 8f38b35..0000000 --- a/ansible/roles/postgresql/handlers/main.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -- name: restart postgresql - systemd: - name: postgresql - state: restarted - daemon_reload: true - -- name: reload postgresql - systemd: - name: postgresql - state: reloaded - -- name: restart pgbouncer - systemd: - name: pgbouncer - state: restarted - -- name: reload systemd - systemd: - daemon_reload: true - -- name: reload sysctl - command: sysctl -p /etc/sysctl.d/30-postgresql.conf - -- name: reload udev - command: udevadm control --reload-rules && udevadm trigger \ No newline at end of file diff --git a/ansible/roles/postgresql/tasks/1password.yml b/ansible/roles/postgresql/tasks/1password.yml deleted file mode 100644 index cd63471..0000000 --- a/ansible/roles/postgresql/tasks/1password.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -# 1Password integration for PostgreSQL passwords -# This file handles password generation and storage in 1Password - -- name: Check if 1Password CLI is available - command: which op - register: op_cli_check - changed_when: false - failed_when: false - delegate_to: localhost - -- name: Fail if 1Password CLI is not installed - fail: - msg: "1Password CLI (op) is not installed. Please install it first." - when: op_cli_check.rc != 0 - -- name: Generate password for PostgreSQL user - set_fact: - generated_password: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits') }}" - when: vault_postgresql_aprsme_password is not defined - no_log: true - -- name: Create 1Password item for PostgreSQL password - shell: | - op item create \ - --category=database \ - --title="PostgreSQL {{ item.name }} - {{ ansible_hostname }}" \ - --vault="Private" \ - username="{{ item.name }}" \ - password="{{ generated_password | default(vault_postgresql_aprsme_password) }}" \ - hostname="{{ ansible_host }}" \ - port="5432" \ - database="{{ postgresql_databases[0].name }}" \ - --account="{{ op_account_id | default('YOOATCZZSVGH7AD6VABUVPORLI') }}" \ - --format=json || \ - op item edit \ - "PostgreSQL {{ item.name }} - {{ ansible_hostname }}" \ - password="{{ generated_password | default(vault_postgresql_aprsme_password) }}" \ - --account="{{ op_account_id | default('YOOATCZZSVGH7AD6VABUVPORLI') }}" - delegate_to: localhost - loop: "{{ postgresql_users }}" - when: use_1password | default(false) - no_log: true - register: op_items - -- name: Set password fact from 1Password or generated - set_fact: - postgresql_users_with_passwords: >- - {%- set users = [] -%} - {%- for user in postgresql_users -%} - {%- set _ = user.update({'password': generated_password | default(vault_postgresql_aprsme_password)}) -%} - {%- set _ = users.append(user) -%} - {%- endfor -%} - {{ users }} - no_log: true \ No newline at end of file diff --git a/ansible/roles/postgresql/tasks/main.yml b/ansible/roles/postgresql/tasks/main.yml deleted file mode 100644 index c32e7df..0000000 --- a/ansible/roles/postgresql/tasks/main.yml +++ /dev/null @@ -1,372 +0,0 @@ ---- -# Set up storage before installing PostgreSQL -- name: Set up storage for PostgreSQL - include_tasks: storage.yml - when: postgresql_storage_setup_enabled | default(false) - -- name: Download PostgreSQL GPG key - ansible.builtin.get_url: - url: https://www.postgresql.org/media/keys/ACCC4CF8.asc - dest: /usr/share/keyrings/postgresql-archive-keyring.asc - mode: '0644' - -- name: Add PostgreSQL repository - ansible.builtin.deb822_repository: - name: pgdg - types: deb - uris: http://apt.postgresql.org/pub/repos/apt - suites: "{{ ansible_distribution_release }}-pgdg" - components: main - signed_by: /usr/share/keyrings/postgresql-archive-keyring.asc - state: present - -- name: Install PostgreSQL and PostGIS - apt: - name: - - postgresql-{{ postgresql_version }} - - postgresql-client-{{ postgresql_version }} - - postgresql-contrib-{{ postgresql_version }} - - postgresql-{{ postgresql_version }}-postgis-3 - - postgresql-{{ postgresql_version }}-postgis-3-scripts - - postgis - - python3-psycopg2 - - libpq-dev - - pgbouncer - state: present - update_cache: true - -- name: Stop PostgreSQL service if running - systemd: - name: postgresql - state: stopped - failed_when: false - -- name: Disable PostgreSQL from starting automatically (we'll configure it first) - systemd: - name: postgresql - enabled: false - -- name: Remove any default PostgreSQL data directory - file: - path: /var/lib/postgresql/{{ postgresql_version }}/main - state: absent - -- name: Install locales package - apt: - name: locales - state: present - -- name: Generate en_US.UTF-8 locale - locale_gen: - name: en_US.UTF-8 - state: present - -- name: Create PostgreSQL config directory structure - file: - path: "{{ item }}" - state: directory - owner: postgres - group: postgres - mode: '0755' - loop: - - /etc/postgresql - - /etc/postgresql/{{ postgresql_version }} - - /etc/postgresql/{{ postgresql_version }}/main - -- name: Create PostgreSQL cluster on RAID array - command: | - /usr/lib/postgresql/{{ postgresql_version }}/bin/initdb \ - -D {{ postgresql_data_directory }} \ - --wal-segsize=64 \ - --data-checksums \ - --encoding=UTF8 \ - --locale=C.UTF-8 \ - --lc-collate=C.UTF-8 \ - --lc-ctype=C.UTF-8 - args: - creates: "{{ postgresql_data_directory }}/PG_VERSION" - become_user: postgres - environment: - PGDATA: "{{ postgresql_data_directory }}" - -- name: Set up SSL/TLS - include_tasks: ssl.yml - -- name: Configure PostgreSQL - template: - src: postgresql.conf.j2 - dest: "{{ postgresql_config_directory }}/postgresql.conf" - owner: postgres - group: postgres - mode: '0644' - notify: restart postgresql - -- name: Configure PostgreSQL authentication - template: - src: pg_hba.conf.j2 - dest: "{{ postgresql_config_directory }}/pg_hba.conf" - owner: postgres - group: postgres - mode: '0640' - notify: restart postgresql - -- name: Create sysctl configuration for PostgreSQL - template: - src: 30-postgresql.conf.j2 - dest: /etc/sysctl.d/30-postgresql.conf - mode: '0644' - notify: reload sysctl - -- name: Create systemd override directory - file: - path: /etc/systemd/system/postgresql@.service.d - state: directory - mode: '0755' - -- name: Configure systemd limits for PostgreSQL - template: - src: postgresql-override.conf.j2 - dest: /etc/systemd/system/postgresql@.service.d/override.conf - mode: '0644' - notify: - - reload systemd - - restart postgresql - -- name: Check if PostgreSQL cluster is properly initialized - stat: - path: "{{ postgresql_data_directory }}/PG_VERSION" - register: pg_initialized - -- name: Reinitialize PostgreSQL if corrupted - block: - - name: Stop PostgreSQL if running - systemd: - name: postgresql@{{ postgresql_version }}-main - state: stopped - failed_when: false - - - name: Remove corrupted data directory - file: - path: "{{ postgresql_data_directory }}" - state: absent - - - name: Create data directory - file: - path: "{{ postgresql_data_directory }}" - state: directory - owner: postgres - group: postgres - mode: '0700' - - - name: Clear WAL directory for reinit - file: - path: "{{ postgresql_wal_directory }}" - state: absent - - - name: Create fresh WAL directory - file: - path: "{{ postgresql_wal_directory }}" - state: directory - owner: postgres - group: postgres - mode: '0700' - - - name: Initialize new PostgreSQL cluster - command: | - /usr/lib/postgresql/{{ postgresql_version }}/bin/initdb \ - -D {{ postgresql_data_directory }} \ - --wal-segsize=64 \ - --data-checksums \ - --encoding=UTF8 \ - --locale=C.UTF-8 \ - --lc-collate=C.UTF-8 \ - --lc-ctype=C.UTF-8 \ - --waldir={{ postgresql_wal_directory }} - become_user: postgres - environment: - PGDATA: "{{ postgresql_data_directory }}" - when: pg_initialized.stat.exists and force_reinit | default(false) - -- name: Ensure WAL directory is properly configured - block: - - name: Check if WAL directory exists - stat: - path: "{{ postgresql_wal_directory }}" - register: wal_dir_stat - - - name: Create WAL directory if missing - file: - path: "{{ postgresql_wal_directory }}" - state: directory - owner: postgres - group: postgres - mode: '0700' - when: not wal_dir_stat.stat.exists - - - name: Check if pg_wal is a symlink - stat: - path: "{{ postgresql_data_directory }}/pg_wal" - register: pg_wal_stat - - - name: Ensure pg_wal symlink has correct ownership - file: - path: "{{ postgresql_data_directory }}/pg_wal" - owner: postgres - group: postgres - when: pg_wal_stat.stat.exists and pg_wal_stat.stat.islnk - -- name: Start and enable PostgreSQL - systemd: - name: postgresql - state: started - enabled: true - daemon_reload: true - -- name: Wait for PostgreSQL to start - wait_for: - port: 5432 - host: localhost - delay: 5 - -- name: Handle password management with 1Password - include_tasks: 1password.yml - when: use_1password | default(false) - -- name: Create PostgreSQL users - postgresql_user: - name: "{{ item.name }}" - password: "{{ item.password }}" - encrypted: true - state: present - become: true - become_user: postgres - loop: "{{ postgresql_users }}" - no_log: true - -- name: Create databases - postgresql_db: - name: "{{ item.name }}" - owner: "{{ item.owner }}" - encoding: UTF-8 - lc_collate: en_US.UTF-8 - lc_ctype: en_US.UTF-8 - template: template0 - state: present - become: true - become_user: postgres - loop: "{{ postgresql_databases }}" - -- name: Enable PostGIS extension - postgresql_ext: - name: postgis - db: "{{ item.name }}" - state: present - become: true - become_user: postgres - loop: "{{ postgresql_databases }}" - -- name: Enable additional extensions - postgresql_ext: - name: "{{ item[1] }}" - db: "{{ item[0].name }}" - state: present - become: true - become_user: postgres - loop: "{{ postgresql_databases | product(['pg_stat_statements', 'btree_gist', 'btree_gin']) | list }}" - -- name: Grant privileges - postgresql_privs: - database: "{{ priv_item.split(':')[0] }}" - privs: "{{ priv_item.split(':')[1] }}" - type: database - role: "{{ user.name }}" - state: present - become: true - become_user: postgres - loop: "{{ postgresql_users | selectattr('priv', 'defined') | map(attribute='priv') | list }}" - loop_control: - loop_var: priv_item - vars: - user: "{{ postgresql_users | selectattr('priv', 'equalto', priv_item) | first }}" - -- name: Retrieve SCRAM hashes for PgBouncer - postgresql_query: - db: postgres - query: "SELECT rolname, rolpassword FROM pg_authid WHERE rolname = %s" - positional_args: - - "{{ item.name }}" - become: true - become_user: postgres - loop: "{{ postgresql_users }}" - register: scram_hashes - no_log: true - -- name: Configure PgBouncer - template: - src: pgbouncer.ini.j2 - dest: /etc/pgbouncer/pgbouncer.ini - owner: postgres - group: postgres - mode: '0640' - notify: restart pgbouncer - -- name: Configure PgBouncer authentication - template: - src: pgbouncer-userlist.txt.j2 - dest: /etc/pgbouncer/userlist.txt - owner: postgres - group: postgres - mode: '0640' - notify: restart pgbouncer - -- name: Enable and start PgBouncer - systemd: - name: pgbouncer - state: started - enabled: true - -- name: Set up backup script - template: - src: backup-postgresql.sh.j2 - dest: /usr/local/bin/backup-postgresql - mode: '0755' - when: postgresql_backup_enabled - -- name: Create backup directory - file: - path: "{{ postgresql_backup_directory }}" - state: directory - owner: postgres - group: postgres - mode: '0755' - when: postgresql_backup_enabled - -- name: Set up backup cron job - cron: - name: "PostgreSQL backup" - minute: "{{ postgresql_backup_schedule.minute }}" - hour: "{{ postgresql_backup_schedule.hour }}" - day: "{{ postgresql_backup_schedule.day }}" - month: "{{ postgresql_backup_schedule.month }}" - weekday: "{{ postgresql_backup_schedule.weekday }}" - user: postgres - job: "/usr/local/bin/backup-postgresql" - when: postgresql_backup_enabled - -- name: Configure log rotation - template: - src: postgresql-logrotate.j2 - dest: /etc/logrotate.d/postgresql-custom - mode: '0644' - -- name: Install monitoring script - template: - src: check-postgresql.sh.j2 - dest: /usr/local/bin/check-postgresql - mode: '0755' - -- name: Set up monitoring cron job - cron: - name: "Check PostgreSQL health" - minute: "*/5" - job: "/usr/local/bin/check-postgresql || echo 'PostgreSQL health check failed' | logger -t postgresql-health" \ No newline at end of file diff --git a/ansible/roles/postgresql/tasks/ssl.yml b/ansible/roles/postgresql/tasks/ssl.yml deleted file mode 100644 index 3eac65e..0000000 --- a/ansible/roles/postgresql/tasks/ssl.yml +++ /dev/null @@ -1,70 +0,0 @@ ---- -# SSL/TLS configuration for PostgreSQL - -- name: Install OpenSSL - apt: - name: openssl - state: present - -- name: Create SSL directory - file: - path: "{{ postgresql_data_directory }}/ssl" - state: directory - owner: postgres - group: postgres - mode: '0700' - -- name: Check if SSL certificate exists - stat: - path: "{{ postgresql_data_directory }}/ssl/server.crt" - register: ssl_cert - -- name: Generate private key for PostgreSQL - shell: | - openssl genrsa -out {{ postgresql_data_directory }}/ssl/server.key 4096 - chmod 600 {{ postgresql_data_directory }}/ssl/server.key - chown postgres:postgres {{ postgresql_data_directory }}/ssl/server.key - when: not ssl_cert.stat.exists - -- name: Generate self-signed certificate - shell: | - openssl req -new -x509 -days 3650 \ - -key {{ postgresql_data_directory }}/ssl/server.key \ - -out {{ postgresql_data_directory }}/ssl/server.crt \ - -subj "/C=US/ST=State/L=City/O=Organization/CN={{ ansible_hostname }}" \ - -addext "subjectAltName=DNS:{{ ansible_hostname }},DNS:{{ ansible_fqdn | default(ansible_hostname) }},IP:{{ ansible_default_ipv4.address }},IP:{{ ansible_host }}" - chmod 644 {{ postgresql_data_directory }}/ssl/server.crt - chown postgres:postgres {{ postgresql_data_directory }}/ssl/server.crt - when: not ssl_cert.stat.exists - -- name: Create CA certificate (copy server cert as CA for self-signed) - copy: - src: "{{ postgresql_data_directory }}/ssl/server.crt" - dest: "{{ postgresql_data_directory }}/ssl/ca.crt" - remote_src: true - owner: postgres - group: postgres - mode: '0644' - -- name: Set proper permissions on SSL files - file: - path: "{{ item.path }}" - owner: postgres - group: postgres - mode: "{{ item.mode }}" - loop: - - { path: "{{ postgresql_data_directory }}/ssl/server.key", mode: '0600' } - - { path: "{{ postgresql_data_directory }}/ssl/server.crt", mode: '0644' } - - { path: "{{ postgresql_data_directory }}/ssl/ca.crt", mode: '0644' } - -- name: Create symlinks in PostgreSQL config directory - file: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - state: link - owner: postgres - group: postgres - loop: - - { src: "{{ postgresql_data_directory }}/ssl/server.crt", dest: "{{ postgresql_config_directory }}/server.crt" } - - { src: "{{ postgresql_data_directory }}/ssl/server.key", dest: "{{ postgresql_config_directory }}/server.key" } - - { src: "{{ postgresql_data_directory }}/ssl/ca.crt", dest: "{{ postgresql_config_directory }}/ca.crt" } \ No newline at end of file diff --git a/ansible/roles/postgresql/tasks/storage.yml b/ansible/roles/postgresql/tasks/storage.yml deleted file mode 100644 index edc59fe..0000000 --- a/ansible/roles/postgresql/tasks/storage.yml +++ /dev/null @@ -1,113 +0,0 @@ ---- -# Storage setup for PostgreSQL on NVMe drives - -- name: Install storage utilities - apt: - name: - - mdadm - - xfsprogs - - nvme-cli - - smartmontools - state: present - update_cache: true - -- name: Check for NVMe devices - shell: nvme list -o json - register: nvme_devices - changed_when: false - failed_when: false - -- name: Create partition on NVMe drive - parted: - device: /dev/nvme0n1 - number: 1 - state: present - label: gpt - part_start: 0% - part_end: 100% - fs_type: ext4 - -- name: Create RAID0 array with single drive (for future expansion) - command: | - mdadm --create {{ postgresql_raid_device }} \ - --level=0 \ - --raid-devices=1 \ - --force \ - /dev/nvme0n1p1 - args: - creates: "{{ postgresql_raid_device }}" - when: postgresql_storage_type == "raid0" - -- name: Wait for RAID array to be ready - wait_for: - path: "{{ postgresql_raid_device }}" - state: present - when: postgresql_storage_type == "raid0" - -- name: Create filesystem on storage device - filesystem: - fstype: xfs - dev: "{{ postgresql_storage_device }}" - opts: "-L pgdata" - when: postgresql_storage_device is defined - -- name: Get filesystem UUID - command: blkid -s UUID -o value {{ postgresql_storage_device }} - register: fs_uuid - changed_when: false - when: postgresql_storage_device is defined - -- name: Create mount points - file: - path: "{{ item }}" - state: directory - owner: root - group: root - mode: '0755' - loop: - - "{{ postgresql_mount_point }}" - - "{{ postgresql_data_directory }}" - - "{{ postgresql_wal_directory }}" - -- name: Mount PostgreSQL storage - mount: - path: "{{ postgresql_mount_point }}" - src: "UUID={{ fs_uuid.stdout }}" - fstype: xfs - opts: "noatime,nodiratime" - state: mounted - when: fs_uuid.stdout is defined - -- name: Save RAID configuration - shell: mdadm --detail --scan >> /etc/mdadm/mdadm.conf - when: postgresql_storage_type == "raid0" - -- name: Update initramfs - command: update-initramfs -u - when: postgresql_storage_type == "raid0" - -- name: Set optimal I/O scheduler for NVMe - copy: - content: | - ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none" - ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/nr_requests}="1024" - dest: /etc/udev/rules.d/60-nvme-scheduler.rules - mode: '0644' - notify: reload udev - -- name: Configure sysctl for PostgreSQL performance - sysctl: - name: "{{ item.name }}" - value: "{{ item.value }}" - state: present - reload: true - loop: - - { name: 'vm.dirty_background_ratio', value: '5' } - - { name: 'vm.dirty_ratio', value: '10' } - - { name: 'vm.swappiness', value: '10' } - - { name: 'vm.overcommit_memory', value: '2' } - - { name: 'vm.overcommit_ratio', value: '95' } - register: sysctl_result - failed_when: - - sysctl_result.failed - - "'No such file or directory' not in sysctl_result.msg | default('')" \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/30-postgresql.conf.j2 b/ansible/roles/postgresql/templates/30-postgresql.conf.j2 deleted file mode 100644 index 51c18da..0000000 --- a/ansible/roles/postgresql/templates/30-postgresql.conf.j2 +++ /dev/null @@ -1,34 +0,0 @@ -# Kernel parameters for PostgreSQL optimization -# For CM3588 with 16GB RAM and RAID0 SSDs - -# Shared memory settings -kernel.shmmax = 17179869184 # 16GB -kernel.shmall = 4194304 # 16GB / 4096 (page size) -kernel.shmmni = 4096 - -# Semaphores: semmsl, semmns, semopm, semmni -kernel.sem = 250 32000 100 128 - -# Virtual memory -vm.swappiness = 10 -vm.dirty_ratio = 15 -vm.dirty_background_ratio = 5 -vm.overcommit_memory = 2 -vm.overcommit_ratio = 95 - -# Huge pages (optional, can be enabled if needed) -# vm.nr_hugepages = 4096 # 8GB of huge pages - -# Network optimization for 2.5Gbit connection -net.core.rmem_default = 262144 -net.core.rmem_max = 4194304 -net.core.wmem_default = 262144 -net.core.wmem_max = 4194304 -net.ipv4.tcp_rmem = 4096 87380 4194304 -net.ipv4.tcp_wmem = 4096 65536 4194304 -net.ipv4.tcp_keepalive_time = 60 -net.ipv4.tcp_keepalive_intvl = 10 -net.ipv4.tcp_keepalive_probes = 6 - -# File system -fs.file-max = 65536 \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/backup-postgresql.sh.j2 b/ansible/roles/postgresql/templates/backup-postgresql.sh.j2 deleted file mode 100644 index 874649c..0000000 --- a/ansible/roles/postgresql/templates/backup-postgresql.sh.j2 +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -# PostgreSQL backup script for APRS.me - -set -euo pipefail - -# Configuration -BACKUP_DIR="{{ postgresql_backup_directory }}" -DB_NAME="aprsme_prod" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz" -KEEP_DAYS=7 - -# Create backup directory if it doesn't exist -mkdir -p "${BACKUP_DIR}" - -# Log backup start -echo "[$(date)] Starting backup of ${DB_NAME}" | logger -t postgresql-backup - -# Perform backup with compression -if pg_dump -Fc -Z9 "${DB_NAME}" > "${BACKUP_FILE}.tmp"; then - mv "${BACKUP_FILE}.tmp" "${BACKUP_FILE}" - echo "[$(date)] Backup completed: ${BACKUP_FILE}" | logger -t postgresql-backup - - # Calculate backup size - SIZE=$(du -h "${BACKUP_FILE}" | cut -f1) - echo "[$(date)] Backup size: ${SIZE}" | logger -t postgresql-backup -else - echo "[$(date)] Backup failed for ${DB_NAME}" | logger -t postgresql-backup -p user.err - rm -f "${BACKUP_FILE}.tmp" - exit 1 -fi - -# Clean up old backups -echo "[$(date)] Cleaning up backups older than ${KEEP_DAYS} days" | logger -t postgresql-backup -find "${BACKUP_DIR}" -name "${DB_NAME}_*.sql.gz" -type f -mtime +${KEEP_DAYS} -delete - -# Verify backup integrity -if pg_restore -l "${BACKUP_FILE}" > /dev/null 2>&1; then - echo "[$(date)] Backup verification successful" | logger -t postgresql-backup -else - echo "[$(date)] Backup verification failed" | logger -t postgresql-backup -p user.err - exit 1 -fi - -# Optional: Copy to remote backup location -# rsync -av "${BACKUP_FILE}" user@backup-server:/path/to/backups/ - -exit 0 \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/check-postgresql.sh.j2 b/ansible/roles/postgresql/templates/check-postgresql.sh.j2 deleted file mode 100644 index 4d3e862..0000000 --- a/ansible/roles/postgresql/templates/check-postgresql.sh.j2 +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# PostgreSQL health check script - -set -euo pipefail - -# Check if PostgreSQL is running -if ! systemctl is-active --quiet postgresql; then - echo "PostgreSQL service is not running" - exit 1 -fi - -# Check if we can connect to PostgreSQL -if ! pg_isready -h localhost -p 5432 -U postgres -q; then - echo "Cannot connect to PostgreSQL" - exit 1 -fi - -# Check database connection -if ! psql -h localhost -p 5432 -U postgres -d aprsme_prod -c "SELECT 1" > /dev/null 2>&1; then - echo "Cannot connect to aprsme_prod database" - exit 1 -fi - -# Check PgBouncer -if ! pg_isready -h localhost -p 6432 -U postgres -q; then - echo "PgBouncer is not responding" - exit 1 -fi - -# Check replication lag (if applicable) -LAG=$(psql -h localhost -p 5432 -U postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - pg_last_xact_replay_timestamp()))::INT" 2>/dev/null || echo "0") -if [ "${LAG}" -gt "60" ]; then - echo "Replication lag is too high: ${LAG} seconds" - exit 1 -fi - -# Check disk space -DISK_USAGE=$(df -h /var/lib/postgresql | tail -1 | awk '{print $5}' | sed 's/%//') -if [ "${DISK_USAGE}" -gt "90" ]; then - echo "Disk usage is critical: ${DISK_USAGE}%" - exit 1 -fi - -# Check connection count -CONNECTIONS=$(psql -h localhost -p 5432 -U postgres -t -c "SELECT COUNT(*) FROM pg_stat_activity" 2>/dev/null || echo "0") -MAX_CONNECTIONS=$(psql -h localhost -p 5432 -U postgres -t -c "SHOW max_connections" 2>/dev/null || echo "200") -if [ "${CONNECTIONS}" -gt "$(( ${MAX_CONNECTIONS} * 90 / 100 ))" ]; then - echo "Connection count is high: ${CONNECTIONS}/${MAX_CONNECTIONS}" - exit 1 -fi - -# Check for long running queries -LONG_QUERIES=$(psql -h localhost -p 5432 -U postgres -t -c "SELECT COUNT(*) FROM pg_stat_activity WHERE state != 'idle' AND query_start < NOW() - INTERVAL '5 minutes'" 2>/dev/null || echo "0") -if [ "${LONG_QUERIES}" -gt "5" ]; then - echo "Too many long running queries: ${LONG_QUERIES}" - exit 1 -fi - -echo "PostgreSQL health check passed" -exit 0 \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/pg_hba.conf.j2 b/ansible/roles/postgresql/templates/pg_hba.conf.j2 deleted file mode 100644 index d8a6fc5..0000000 --- a/ansible/roles/postgresql/templates/pg_hba.conf.j2 +++ /dev/null @@ -1,9 +0,0 @@ -# PostgreSQL Client Authentication Configuration File -# =================================================== -# Managed by Ansible - Do not edit manually - -# TYPE DATABASE USER ADDRESS METHOD - -{% for entry in postgresql_hba_entries %} -{{ entry.type }} {{ entry.database }} {{ entry.user }} {{ entry.address | default('') }} {{ entry.auth_method }} -{% endfor %} \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/pgbouncer-userlist.txt.j2 b/ansible/roles/postgresql/templates/pgbouncer-userlist.txt.j2 deleted file mode 100644 index 2c65f43..0000000 --- a/ansible/roles/postgresql/templates/pgbouncer-userlist.txt.j2 +++ /dev/null @@ -1,13 +0,0 @@ -# PgBouncer userlist file -# Format: "username" "password_or_scram_hash" -# Managed by Ansible -# Note: When using SCRAM-SHA-256, this contains the SCRAM hash from pg_authid - -{% for result in scram_hashes.results %} -{% if result.query_result[0].rolpassword %} -"{{ result.query_result[0].rolname }}" "{{ result.query_result[0].rolpassword }}" -{% endif %} -{% endfor %} - -# PgBouncer stats user -"pgbouncer" "pgbouncer" \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/pgbouncer.ini.j2 b/ansible/roles/postgresql/templates/pgbouncer.ini.j2 deleted file mode 100644 index 16c3954..0000000 --- a/ansible/roles/postgresql/templates/pgbouncer.ini.j2 +++ /dev/null @@ -1,73 +0,0 @@ -[databases] -# Connection string for aprsme database -aprsme_prod = host=127.0.0.1 port=5432 dbname=aprsme_prod - -[pgbouncer] -# Connection settings -listen_addr = * -listen_port = 6432 -auth_type = scram-sha-256 -auth_file = /etc/pgbouncer/userlist.txt - -# Pool settings optimized for APRS.me -pool_mode = transaction -max_client_conn = 1000 -default_pool_size = 25 -min_pool_size = 5 -reserve_pool_size = 5 -reserve_pool_timeout = 3 - -# Performance settings -max_db_connections = 100 -max_user_connections = 100 -server_lifetime = 3600 -server_idle_timeout = 600 -server_connect_timeout = 15 -server_login_retry = 15 -query_timeout = 0 -query_wait_timeout = 120 -client_idle_timeout = 0 -client_login_timeout = 60 - -# Logging -log_connections = 1 -log_disconnections = 1 -log_pooler_errors = 1 -stats_period = 60 - -# Admin settings -admin_users = postgres -stats_users = postgres, aprsme - -# Security -ignore_startup_parameters = extra_float_digits - -# Resource limits -pkt_buf = 4096 -max_packet_size = 2147483647 -listen_backlog = 128 -sbuf_loopcnt = 5 -tcp_defer_accept = 0 -tcp_socket_buffer = 0 -tcp_keepalive = 1 -tcp_keepcnt = 3 -tcp_keepidle = 60 -tcp_keepintvl = 10 - -# DNS settings -dns_max_ttl = 15 -dns_nxdomain_ttl = 15 - -# TLS settings for client connections -client_tls_sslmode = allow -client_tls_cert_file = /etc/postgresql/{{ postgresql_version }}/main/server.crt -client_tls_key_file = /etc/postgresql/{{ postgresql_version }}/main/server.key -client_tls_ca_file = /etc/postgresql/{{ postgresql_version }}/main/ca.crt -client_tls_protocols = secure -client_tls_ciphers = HIGH:MEDIUM:+3DES:!aNULL - -# TLS settings for server connections -server_tls_sslmode = require -server_tls_ca_file = /etc/postgresql/{{ postgresql_version }}/main/ca.crt -server_tls_protocols = secure -server_tls_ciphers = HIGH:MEDIUM:+3DES:!aNULL \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/postgresql-logrotate.j2 b/ansible/roles/postgresql/templates/postgresql-logrotate.j2 deleted file mode 100644 index 1dc4c04..0000000 --- a/ansible/roles/postgresql/templates/postgresql-logrotate.j2 +++ /dev/null @@ -1,16 +0,0 @@ -/var/log/postgresql/*.log { - daily - rotate 7 - missingok - compress - delaycompress - notifempty - create 640 postgres postgres - sharedscripts - postrotate - # Signal PostgreSQL to reopen log files - if [ -f /var/run/postgresql/{{ postgresql_version }}-main.pid ]; then - /usr/bin/pg_ctlcluster {{ postgresql_version }} main reload > /dev/null - fi - endscript -} \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/postgresql-override.conf.j2 b/ansible/roles/postgresql/templates/postgresql-override.conf.j2 deleted file mode 100644 index cc3e0e5..0000000 --- a/ansible/roles/postgresql/templates/postgresql-override.conf.j2 +++ /dev/null @@ -1,17 +0,0 @@ -[Service] -# Increase limits for PostgreSQL -LimitNOFILE=65536 -LimitNPROC=4096 - -# Disable OOM killer for PostgreSQL -OOMScoreAdjust=-1000 - -# CPU affinity - use all cores -CPUAffinity=0-7 - -# Nice level for better priority -Nice=-5 - -# I/O scheduling class and priority -IOSchedulingClass=best-effort -IOSchedulingPriority=0 \ No newline at end of file diff --git a/ansible/roles/postgresql/templates/postgresql.conf.j2 b/ansible/roles/postgresql/templates/postgresql.conf.j2 deleted file mode 100644 index 23866e3..0000000 --- a/ansible/roles/postgresql/templates/postgresql.conf.j2 +++ /dev/null @@ -1,115 +0,0 @@ -# PostgreSQL configuration for APRS.me on CM3588 -# Optimized for RK3588 with RAID0 SSDs - -#------------------------------------------------------------------------------ -# FILE LOCATIONS -#------------------------------------------------------------------------------ - -data_directory = '{{ postgresql_data_directory }}' -hba_file = '{{ postgresql_config_directory }}/pg_hba.conf' -ident_file = '{{ postgresql_config_directory }}/pg_ident.conf' -external_pid_file = '/var/run/postgresql/{{ postgresql_version }}-main.pid' - -#------------------------------------------------------------------------------ -# CONNECTIONS AND AUTHENTICATION -#------------------------------------------------------------------------------ - -{% for key, value in postgresql_config.items() %} -{{ key }} = {{ value }} -{% endfor %} - -#------------------------------------------------------------------------------ -# RESOURCE USAGE (except WAL) -#------------------------------------------------------------------------------ - -# These settings are configured above in postgresql_config - -#------------------------------------------------------------------------------ -# WRITE-AHEAD LOG -#------------------------------------------------------------------------------ - -# WAL settings configured above - -#------------------------------------------------------------------------------ -# REPLICATION -#------------------------------------------------------------------------------ - -wal_level = replica -archive_mode = on -archive_command = 'test ! -f {{ postgresql_archive_directory }}/%f && cp %p {{ postgresql_archive_directory }}/%f' -max_wal_senders = 3 -wal_keep_size = 1GB - -#------------------------------------------------------------------------------ -# QUERY TUNING -#------------------------------------------------------------------------------ - -# Planner settings for PostGIS and complex queries -enable_partitionwise_join = on -enable_partitionwise_aggregate = on -jit = on - -#------------------------------------------------------------------------------ -# REPORTING AND LOGGING -#------------------------------------------------------------------------------ - -# Logging configuration set above - -#------------------------------------------------------------------------------ -# STATISTICS -#------------------------------------------------------------------------------ - -# Stats configuration set above -shared_preload_libraries = 'pg_stat_statements' -pg_stat_statements.max = 10000 -pg_stat_statements.track = all - -#------------------------------------------------------------------------------ -# AUTOVACUUM -#------------------------------------------------------------------------------ - -# Autovacuum settings configured above - -#------------------------------------------------------------------------------ -# CLIENT CONNECTION DEFAULTS -#------------------------------------------------------------------------------ - -datestyle = 'iso, mdy' -timezone = 'UTC' -lc_messages = 'en_US.UTF-8' -lc_monetary = 'en_US.UTF-8' -lc_numeric = 'en_US.UTF-8' -lc_time = 'en_US.UTF-8' -default_text_search_config = 'pg_catalog.english' - -#------------------------------------------------------------------------------ -# LOCK MANAGEMENT -#------------------------------------------------------------------------------ - -deadlock_timeout = 1s -max_locks_per_transaction = 128 - -#------------------------------------------------------------------------------ -# VERSION/PLATFORM COMPATIBILITY -#------------------------------------------------------------------------------ - -# No specific settings needed - -#------------------------------------------------------------------------------ -# ERROR HANDLING -#------------------------------------------------------------------------------ - -exit_on_error = off -restart_after_crash = on - -#------------------------------------------------------------------------------ -# SSL/TLS CONFIGURATION -#------------------------------------------------------------------------------ - -ssl = on -ssl_cert_file = '/etc/postgresql/{{ postgresql_version }}/main/server.crt' -ssl_key_file = '/etc/postgresql/{{ postgresql_version }}/main/server.key' -ssl_ca_file = '/etc/postgresql/{{ postgresql_version }}/main/ca.crt' -ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' -ssl_prefer_server_ciphers = on -ssl_min_protocol_version = 'TLSv1.2' \ No newline at end of file diff --git a/ansible/roles/resolvers/handlers/main.yml b/ansible/roles/resolvers/handlers/main.yml index 05afc08..77230f0 100755 --- a/ansible/roles/resolvers/handlers/main.yml +++ b/ansible/roles/resolvers/handlers/main.yml @@ -1,15 +1,11 @@ --- -# Handler for the webtier: handlers are called by other plays. -# See http://docs.ansible.com/playbooks_intro.html for more information about handlers. - - name: Restart unbound ansible.builtin.service: name: unbound state: restarted - become: true - name: Restart firewalld ansible.builtin.service: name: firewalld state: restarted - become: true + diff --git a/ansible/roles/resolvers/tasks/main.yml b/ansible/roles/resolvers/tasks/main.yml index 2ea90ab..c2b5dc9 100755 --- a/ansible/roles/resolvers/tasks/main.yml +++ b/ansible/roles/resolvers/tasks/main.yml @@ -3,14 +3,31 @@ ansible.builtin.dnf: name: - unbound - - certbot state: present -- name: Enable service Unbound and ensure it is not masked +- name: Create systemd override directory for unbound + ansible.builtin.file: + path: /etc/systemd/system/unbound.service.d + state: directory + owner: root + group: root + mode: '0755' + +- name: Fix unbound sd_notify failure (disable notify type) + ansible.builtin.copy: + dest: /etc/systemd/system/unbound.service.d/override.conf + content: | + [Service] + Type=simple + owner: root + group: root + mode: '0644' + register: unbound_override + +- name: Reload systemd after override change ansible.builtin.systemd: - name: unbound - enabled: true - masked: false + daemon_reload: true + when: unbound_override.changed - name: Create Unbound config ansible.builtin.template: @@ -20,25 +37,18 @@ notify: - Restart unbound +- name: Enable and start Unbound + ansible.builtin.systemd: + name: unbound + state: started + enabled: true + - name: Permit traffic in default zone for dns service ansible.posix.firewalld: zone: public service: dns permanent: true state: enabled - -- name: Permit traffic in default zone for http service - ansible.posix.firewalld: - zone: public - service: http - permanent: true - state: enabled - -- name: Permit traffic in default zone for https service - ansible.posix.firewalld: - zone: public - service: https - permanent: true - state: enabled + when: ansible_facts.services['firewalld.service'] is defined notify: - Restart firewalld diff --git a/ansible/roles/uisp/defaults/main.yml b/ansible/roles/uisp/defaults/main.yml new file mode 100644 index 0000000..b19ea5d --- /dev/null +++ b/ansible/roles/uisp/defaults/main.yml @@ -0,0 +1,8 @@ +--- +uisp_user: unms +uisp_user_uid: 1001 +uisp_group: unms +uisp_group_gid: 1001 +uisp_home: /home/unms +uisp_app_dir: /home/unms/app +uisp_data_dir: /home/unms/data diff --git a/ansible/roles/uisp/handlers/main.yml b/ansible/roles/uisp/handlers/main.yml new file mode 100644 index 0000000..110c913 --- /dev/null +++ b/ansible/roles/uisp/handlers/main.yml @@ -0,0 +1,14 @@ +--- +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + +- name: Restart containerd + ansible.builtin.service: + name: containerd + state: restarted + +- name: Restart docker + ansible.builtin.service: + name: docker + state: restarted diff --git a/ansible/roles/uisp/tasks/main.yml b/ansible/roles/uisp/tasks/main.yml new file mode 100644 index 0000000..167f6fc --- /dev/null +++ b/ansible/roles/uisp/tasks/main.yml @@ -0,0 +1,103 @@ +--- +- name: Install Docker prerequisites + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + state: present + update_cache: yes + +- name: Add Docker GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/debian/gpg + dest: /etc/apt/keyrings/docker.asc + mode: '0644' + +- name: Add Docker APT repository + ansible.builtin.apt_repository: + repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable" + filename: docker + state: present + +- name: Install Docker CE + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + - docker-ce-rootless-extras + state: present + update_cache: yes + +- name: Create UISP group + ansible.builtin.group: + name: "{{ uisp_group }}" + gid: "{{ uisp_group_gid }}" + state: present + +- name: Create UISP user + ansible.builtin.user: + name: "{{ uisp_user }}" + uid: "{{ uisp_user_uid }}" + group: "{{ uisp_group }}" + groups: docker + append: yes + home: "{{ uisp_home }}" + shell: /bin/sh + state: present + +- name: Ensure Docker is running + ansible.builtin.systemd: + name: docker + state: started + enabled: yes + +- name: Create containerd killmode override directory + ansible.builtin.file: + path: /usr/lib/systemd/system/containerd.service.d + state: directory + owner: root + group: root + mode: '0755' + +- name: Configure containerd KillMode for UISP + ansible.builtin.copy: + dest: /usr/lib/systemd/system/containerd.service.d/unms-killmode.conf + content: | + [Service] + KillMode=mixed + owner: root + group: root + mode: '0644' + notify: + - Reload systemd + - Restart containerd + +- name: Ensure UISP app directory exists + ansible.builtin.file: + path: "{{ uisp_app_dir }}" + state: directory + owner: "{{ uisp_user }}" + group: root + mode: '0700' + +- name: Ensure UISP data directory exists + ansible.builtin.file: + path: "{{ uisp_data_dir }}" + state: directory + owner: "{{ uisp_user }}" + group: root + mode: '0700' + +- name: Deploy UISP auto-update cron job + ansible.builtin.copy: + dest: /etc/cron.d/unms-update + content: | + * * * * * {{ uisp_user }} {{ uisp_app_dir }}/update.sh --cron > /dev/null 2>&1 || true + owner: root + group: root + mode: '0644' diff --git a/tofu/cloudflare.tf b/tofu/cloudflare.tf index 4e9735a..6f79a75 100644 --- a/tofu/cloudflare.tf +++ b/tofu/cloudflare.tf @@ -333,7 +333,7 @@ resource "cloudflare_zone_settings_override" "towerops_net" { always_use_https = "on" tls_1_3 = "zrt" zero_rtt = "on" - ssl = "strict" + ssl = "full" security_header { enabled = true @@ -397,6 +397,20 @@ resource "cloudflare_ruleset" "towerops_firewall" { kind = "zone" phase = "http_request_firewall_custom" + # Allow agent WebSocket connections (must come before block rules) + rules { + action = "skip" + action_parameters { + ruleset = "current" + } + expression = "(http.request.uri.path eq \"/socket/agent/websocket\")" + description = "allow agent websocket connections" + enabled = true + logging { + enabled = false + } + } + rules { action = "block" expression = local.firewall_expression diff --git a/tofu/terraform.tfstate b/tofu/terraform.tfstate index 254635f..43f1daf 100644 --- a/tofu/terraform.tfstate +++ b/tofu/terraform.tfstate @@ -1 +1 @@ -{"version":4,"terraform_version":"1.11.5","serial":22,"lineage":"aada9e42-4c37-f0e0-91fa-f89b3caac095","outputs":{},"resources":[{"mode":"managed","type":"cloudflare_record","name":"aprs_me_tunnel","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"d3d819ba-925c-4d84-8052-68ed32b686bb.cfargotunnel.com","created_on":"2026-02-18T18:34:56.202856Z","data":[],"hostname":"aprs.me","id":"52fe06c580f9bc70a4ff4a6978de8ea7","metadata":{},"modified_on":"2026-02-18T18:34:56.202856Z","name":"aprs.me","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"caa","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"index_key":"aprs.me-letsencrypt.org","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"letsencrypt.org\"","created_on":"2026-02-25T19:20:24.920388Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"letsencrypt.org","weight":0}],"hostname":"aprs.me","id":"e9f4343f7c0dd97351b6868d48ac3ffc","metadata":{},"modified_on":"2026-02-25T19:20:24.920388Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"aprs.me-pki.goog","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"pki.goog\"","created_on":"2026-02-25T19:20:24.931937Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"pki.goog","weight":0}],"hostname":"aprs.me","id":"312b7a4e5ef24aa39d0d644448b42ba1","metadata":{},"modified_on":"2026-02-25T19:20:24.931937Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"aprs.me-sectigo.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"sectigo.com\"","created_on":"2026-02-25T19:20:23.4046Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"sectigo.com","weight":0}],"hostname":"aprs.me","id":"a03268c868921324d93c424b75a66123","metadata":{},"modified_on":"2026-02-25T19:20:23.4046Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"aprs.me-ssl.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"ssl.com\"","created_on":"2026-02-25T19:20:23.757538Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"ssl.com","weight":0}],"hostname":"aprs.me","id":"ca710bf09e7b007ed13e55f6cb3b72ba","metadata":{},"modified_on":"2026-02-25T19:20:23.757538Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-letsencrypt.org","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"letsencrypt.org\"","created_on":"2026-02-25T19:20:26.213276Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"letsencrypt.org","weight":0}],"hostname":"gridmap.org","id":"ec4d9e50c72f265010b4dc56db1f21f7","metadata":{},"modified_on":"2026-02-25T19:20:26.213276Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-pki.goog","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"pki.goog\"","created_on":"2026-02-25T19:20:25.34753Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"pki.goog","weight":0}],"hostname":"gridmap.org","id":"bf2e711ac3abb681fc725daa2e0a2c53","metadata":{},"modified_on":"2026-02-25T19:20:25.34753Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-sectigo.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"sectigo.com\"","created_on":"2026-02-25T19:20:26.603772Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"sectigo.com","weight":0}],"hostname":"gridmap.org","id":"8175bf30b3d31b82c4c1b85af252ac7e","metadata":{},"modified_on":"2026-02-25T19:20:26.603772Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-ssl.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"ssl.com\"","created_on":"2026-02-25T19:20:23.273147Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"ssl.com","weight":0}],"hostname":"gridmap.org","id":"03749ab647ee85cdf0bb7ba8bcbba3c1","metadata":{},"modified_on":"2026-02-25T19:20:23.273147Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"gridmap_org_tunnel","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"d3d819ba-925c-4d84-8052-68ed32b686bb.cfargotunnel.com","created_on":"2026-02-18T18:34:53.58717Z","data":[],"hostname":"gridmap.org","id":"8b218c8e8e8a78aa8c30a5e82e63dbdc","metadata":{},"modified_on":"2026-02-18T18:34:53.58717Z","name":"gridmap.org","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"records","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"index_key":"towerops.net-a-dfw-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"144.202.64.79","created_on":"2026-02-04T18:34:31.048255Z","data":[],"hostname":"dfw.poller.towerops.net","id":"aa8183b748f1bf0e3850d9dabb91ee3a","metadata":{},"modified_on":"2026-02-04T18:34:31.048255Z","name":"dfw.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-a-lon-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"45.77.228.168","created_on":"2026-02-08T17:23:07.876902Z","data":[],"hostname":"lon.poller.towerops.net","id":"2a25c3ee47284833cea451170d3bd61d","metadata":{},"modified_on":"2026-02-08T17:23:07.876902Z","name":"lon.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-a-staging","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"204.110.191.231","created_on":"2026-02-09T18:23:25.894749Z","data":[],"hostname":"staging.towerops.net","id":"8430e2789d5700c556dc7a598d49d8fd","metadata":{},"modified_on":"2026-02-09T18:44:11.107671Z","name":"staging","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-a-www","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"Created during Cloudflare Rules deployment process for Redirect from WWW to root [Template]","content":"192.0.2.1","created_on":"2026-02-01T16:08:06.434962Z","data":[],"hostname":"www.towerops.net","id":"689c47b0e5ed68427f2c3b9e25bdf602","metadata":{},"modified_on":"2026-02-01T16:08:06.434962Z","name":"www","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-aaaa-dfw-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"2001:19f0:6401:af2:5400:5ff:fee7:1bfb","created_on":"2026-02-04T18:34:31.174197Z","data":[],"hostname":"dfw.poller.towerops.net","id":"2edc0cf203313e67d64316daf8a0490e","metadata":{},"modified_on":"2026-02-04T18:34:31.174197Z","name":"dfw.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"AAAA","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-aaaa-lon-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"2001:19f0:7400:1195:5400:5ff:feee:a0c3","created_on":"2026-02-08T17:24:02.914425Z","data":[],"hostname":"lon.poller.towerops.net","id":"610f21b8597a1ce6ad4020f15311787a","metadata":{},"modified_on":"2026-02-08T17:24:02.914425Z","name":"lon.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"AAAA","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-autoconfig","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.mcintire.me","created_on":"2026-01-31T21:12:11.834659Z","data":[],"hostname":"autoconfig.towerops.net","id":"1c0f59e7bd583f8971f50231e6fad3cf","metadata":{},"modified_on":"2026-02-18T18:21:20.964531Z","name":"autoconfig","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-autodiscover","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.mcintire.me","created_on":"2026-01-31T21:12:11.813721Z","data":[],"hostname":"autodiscover.towerops.net","id":"34a4a9bde8cf4626635cfa3cb5f8c408","metadata":{},"modified_on":"2026-02-18T18:21:21.959176Z","name":"autodiscover","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dev","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"db59aa44-79de-4522-9593-43e828f3215a.cfargotunnel.com","created_on":"2026-02-01T16:17:24.029892Z","data":[],"hostname":"dev.towerops.net","id":"21b470e19493c856ce697052dabc90e5","metadata":{},"modified_on":"2026-02-18T18:21:15.242958Z","name":"dev","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dkim-ses-1","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"2skkb53ep3vfbkdstfmm2elfthurggid.dkim.amazonses.com","created_on":"2026-02-06T14:56:00.620182Z","data":[],"hostname":"2skkb53ep3vfbkdstfmm2elfthurggid._domainkey.towerops.net","id":"f4a446531128011a1a6b896010983901","metadata":{},"modified_on":"2026-02-18T18:21:23.474077Z","name":"2skkb53ep3vfbkdstfmm2elfthurggid._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dkim-ses-2","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"4uwsaywjpmk35j36pfiz4vr37dyy4v7z.dkim.amazonses.com","created_on":"2026-02-06T14:56:00.429727Z","data":[],"hostname":"4uwsaywjpmk35j36pfiz4vr37dyy4v7z._domainkey.towerops.net","id":"aefc5c72e25e3656dbccf0fdf2fc6f48","metadata":{},"modified_on":"2026-02-18T18:21:18.993371Z","name":"4uwsaywjpmk35j36pfiz4vr37dyy4v7z._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dkim-ses-3","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"l7lbifvot6nv3wwho5rs35xzqzzinz5f.dkim.amazonses.com","created_on":"2026-02-06T14:56:00.400622Z","data":[],"hostname":"l7lbifvot6nv3wwho5rs35xzqzzinz5f._domainkey.towerops.net","id":"02a4cfa316dae29898a8270f2c3aceda","metadata":{},"modified_on":"2026-02-18T18:21:15.477573Z","name":"l7lbifvot6nv3wwho5rs35xzqzzinz5f._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-root","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"d3d819ba-925c-4d84-8052-68ed32b686bb.cfargotunnel.com","created_on":"2026-01-31T21:20:07.097359Z","data":[],"hostname":"towerops.net","id":"cf1bc4e745466032786db854f4294f99","metadata":{},"modified_on":"2026-02-18T18:21:17.216154Z","name":"towerops.net","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-mx-mail","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"feedback-smtp.us-east-1.amazonses.com","created_on":"2026-01-31T21:12:11.92231Z","data":[],"hostname":"mail.towerops.net","id":"a2486e2bbb4d6aca978d28807893e346","metadata":{},"modified_on":"2026-01-31T21:12:11.92231Z","name":"mail","priority":10,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"MX","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-mx-root-backup","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.nsnw.ca","created_on":"2026-01-31T21:12:11.783987Z","data":[],"hostname":"towerops.net","id":"bbd552d191e61345f19f8dae10281f9e","metadata":{},"modified_on":"2026-01-31T21:12:11.783987Z","name":"towerops.net","priority":20,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"MX","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-mx-root-primary","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.mcintire.me","created_on":"2026-01-31T21:12:11.794166Z","data":[],"hostname":"towerops.net","id":"a5dc7d6dc6d9085132a8fa0b7db1b69a","metadata":{},"modified_on":"2026-01-31T21:12:11.794166Z","name":"towerops.net","priority":10,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"MX","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-dkim","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuFm6nd0L7/nAO4m0MPxdjwRAup5C90DPSPa0TQBDT8S2WvASyQUPV8xSbICGMj6FjZYNd5ph/0VK47gDulxQgNQT96IVf2ooTfkvY/80bAyEdtJbpjEUzQnX5YBi/pJigQx2EeCdaojUZumeIdlFlzeb8QQIEtQhVnyc1Xc/Z8ljPMW1H9DXLSDINjV6UTaXX+PZ7jgpRKgU+RnJNcqTAkhhKQ4bMcRfOam/lF6wPOWte1A7ir08TdE8O67ioqygkr/ivAp7xvV4vGOkY6buLLE+c3exY+my2qTCZgJyUMAhkZzYqzGo1csfHH8x+O2XN/k8Z5jPhq8LSvl9XkllKwIDAQAB","created_on":"2026-01-31T21:12:11.932177Z","data":[],"hostname":"dkim._domainkey.towerops.net","id":"3872e537e8cee4087ead7eb5785e1426","metadata":{},"modified_on":"2026-02-03T19:17:59.246248Z","name":"dkim._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-dmarc","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=DMARC1; p=none;","created_on":"2026-01-31T21:12:11.846177Z","data":[],"hostname":"_dmarc.towerops.net","id":"bd236f9a89fc42f45fd7f4b076478880","metadata":{},"modified_on":"2026-02-03T19:17:59.427606Z","name":"_dmarc","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-mail-spf","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=spf1 include:amazonses.com ~all","created_on":"2026-01-31T21:12:11.939949Z","data":[],"hostname":"mail.towerops.net","id":"76b8314432391c6630e9e1b2edfce65d","metadata":{},"modified_on":"2026-02-03T19:18:00.631343Z","name":"mail","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-spf","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=spf1 mx a ip4:107.174.178.20 ~all","created_on":"2026-01-31T21:12:11.825178Z","data":[],"hostname":"towerops.net","id":"cf2f74d345960700a81a6025e7a89642","metadata":{},"modified_on":"2026-02-03T19:17:59.830149Z","name":"towerops.net","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"towerops_srv_autodiscover","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 443 mail.mcintire.me","created_on":"2026-01-31T21:12:11.914868Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":443,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"","target":"mail.mcintire.me","type":0,"usage":0,"value":"","weight":0}],"hostname":"_autodiscover._tcp.towerops.net","id":"26db78978535d1d630432970b9a4e279","metadata":{},"modified_on":"2026-01-31T21:12:11.914868Z","name":"_autodiscover._tcp","priority":0,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"SRV","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_ruleset","name":"aprs_me_firewall","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":1,"attributes":{"account_id":null,"description":"","id":"9a107a7272a14c05b9e0e747b5a93bf7","kind":"zone","name":"default","phase":"http_request_firewall_custom","rules":[{"action":"block","action_parameters":[],"description":"block abusers","enabled":true,"exposed_credential_check":[],"expression":"(ip.src.country in {\"KP\" \"RU\" \"CN\" \"SY\" \"MM\" \"VE\" \"IQ\" \"LY\" \"SO\" \"YE\" \"SS\"}) or (http.request.uri.path contains \".php\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\") or (http.request.uri.path contains \"wlwmanifest.xml\") or (http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git\") or (http.request.uri.path contains \"/phpmyadmin\") or (http.request.uri.path contains \"/cgi-bin\") or (not http.user_agent ne \"\")","id":"6bbd814f49634e3997386d6fa1193e4e","logging":[],"ratelimit":[],"ref":"6bbd814f49634e3997386d6fa1193e4e"}],"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_ruleset","name":"gridmap_firewall","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":1,"attributes":{"account_id":null,"description":"","id":"7dfc79c24a22439ea501865479822df8","kind":"zone","name":"default","phase":"http_request_firewall_custom","rules":[{"action":"block","action_parameters":[],"description":"block abusers","enabled":true,"exposed_credential_check":[],"expression":"(ip.src.country in {\"KP\" \"RU\" \"CN\" \"SY\" \"MM\" \"VE\" \"IQ\" \"LY\" \"SO\" \"YE\" \"SS\"}) or (http.request.uri.path contains \".php\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\") or (http.request.uri.path contains \"wlwmanifest.xml\") or (http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git\") or (http.request.uri.path contains \"/phpmyadmin\") or (http.request.uri.path contains \"/cgi-bin\") or (not http.user_agent ne \"\")","id":"b90e87338cd84bd4a8caad80851eb539","logging":[],"ratelimit":[],"ref":"b90e87338cd84bd4a8caad80851eb539"}],"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_ruleset","name":"towerops_firewall","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":1,"attributes":{"account_id":null,"description":"","id":"3c209e50f21b40388733a7cfe8f62f4e","kind":"zone","name":"default","phase":"http_request_firewall_custom","rules":[{"action":"block","action_parameters":[],"description":"block abusers","enabled":true,"exposed_credential_check":[],"expression":"(ip.src.country in {\"KP\" \"RU\" \"CN\" \"SY\" \"MM\" \"VE\" \"IQ\" \"LY\" \"SO\" \"YE\" \"SS\"}) or (http.request.uri.path contains \".php\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\") or (http.request.uri.path contains \"wlwmanifest.xml\") or (http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git\") or (http.request.uri.path contains \"/phpmyadmin\") or (http.request.uri.path contains \"/cgi-bin\") or (not http.user_agent ne \"\")","id":"7a6d32e7a2cc4ebfbca01d6fae850b80","logging":[],"ratelimit":[],"ref":"7a6d32e7a2cc4ebfbca01d6fae850b80"}],"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_zero_trust_tunnel_cloudflared_config","name":"towerops","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","config":[{"ingress_rule":[{"hostname":"aprs.me","origin_request":[],"path":"","service":"http://aprs.aprs.svc.cluster.local:4000"},{"hostname":"gridmap.org","origin_request":[],"path":"","service":"http://gridmap.gridmap.svc.cluster.local:4000"},{"hostname":"towerops.net","origin_request":[],"path":"","service":"http://towerops.towerops.svc.cluster.local:4000"},{"hostname":"","origin_request":[],"path":"","service":"http_status:404"}],"origin_request":[],"warp_routing":[]}],"id":"d3d819ba-925c-4d84-8052-68ed32b686bb","tunnel_id":"d3d819ba-925c-4d84-8052-68ed32b686bb"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="}]},{"mode":"managed","type":"cloudflare_zero_trust_tunnel_cloudflared_config","name":"towerops_dev","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","config":[{"ingress_rule":[{"hostname":"dev.towerops.net","origin_request":[],"path":"","service":"http://localhost:4000"},{"hostname":"","origin_request":[],"path":"","service":"http_status:404"}],"origin_request":[],"warp_routing":[]}],"id":"db59aa44-79de-4522-9593-43e828f3215a","tunnel_id":"db59aa44-79de-4522-9593-43e828f3215a"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="}]},{"mode":"managed","type":"cloudflare_zone","name":"zones","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"index_key":"aprs.me","schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","id":"09da59049b0ecad7b734ce3d902bf581","jump_start":null,"meta":{"phishing_detected":false,"wildcard_proxiable":false},"name_servers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"],"paused":false,"plan":"free","status":"active","type":"full","vanity_name_servers":[],"verification_key":"","zone":"aprs.me"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="},{"index_key":"gridmap.org","schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","id":"836f24d97eb51ffc18aa10c0a98efa48","jump_start":null,"meta":{"phishing_detected":false,"wildcard_proxiable":false},"name_servers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"],"paused":false,"plan":"free","status":"active","type":"full","vanity_name_servers":[],"verification_key":"","zone":"gridmap.org"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="},{"index_key":"towerops.net","schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","id":"c275d35a8885de79ca8e61c17ac97ab6","jump_start":null,"meta":{"phishing_detected":false,"wildcard_proxiable":false},"name_servers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"],"paused":false,"plan":"free","status":"active","type":"full","vanity_name_servers":[],"verification_key":"","zone":"towerops.net"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="}]},{"mode":"managed","type":"cloudflare_zone_settings_override","name":"aprs_me","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":2,"attributes":{"id":"09da59049b0ecad7b734ce3d902bf581","initial_settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"off","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.0","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":false,"include_subdomains":false,"max_age":0,"nosniff":false,"preload":false}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"on","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"off","websockets":"on","zero_rtt":"off"}],"initial_settings_read_at":"2026-02-25T19:03:59.962087Z","readonly_settings":["advanced_ddos","http2","long_lived_grpc","mirage","origin_error_page_pass_thru","polish","prefetch_preload","proxy_read_timeout","response_buffering","sort_query_string_for_cache","true_client_ip_header","webp","image_resizing"],"settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":true,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"strict","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"","websockets":"on","zero_rtt":"on"}],"zone_id":"09da59049b0ecad7b734ce3d902bf581","zone_status":"active","zone_type":"full"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_zone_settings_override","name":"gridmap_org","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":2,"attributes":{"id":"836f24d97eb51ffc18aa10c0a98efa48","initial_settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"off","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.0","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":false,"include_subdomains":false,"max_age":0,"nosniff":false,"preload":false}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"on","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"off","websockets":"on","zero_rtt":"off"}],"initial_settings_read_at":"2026-02-25T19:04:25.78697Z","readonly_settings":["advanced_ddos","http2","long_lived_grpc","mirage","origin_error_page_pass_thru","polish","prefetch_preload","proxy_read_timeout","response_buffering","sort_query_string_for_cache","true_client_ip_header","webp","image_resizing"],"settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":true,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"strict","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"","websockets":"on","zero_rtt":"on"}],"zone_id":"836f24d97eb51ffc18aa10c0a98efa48","zone_status":"active","zone_type":"full"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_zone_settings_override","name":"towerops_net","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":2,"attributes":{"id":"c275d35a8885de79ca8e61c17ac97ab6","initial_settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"add_header","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":false,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"off","websockets":"on","zero_rtt":"on"}],"initial_settings_read_at":"2026-02-25T19:04:04.422661Z","readonly_settings":["advanced_ddos","http2","long_lived_grpc","mirage","origin_error_page_pass_thru","polish","prefetch_preload","proxy_read_timeout","response_buffering","sort_query_string_for_cache","true_client_ip_header","webp","image_resizing"],"settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"add_header","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":true,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"strict","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"","websockets":"on","zero_rtt":"on"}],"zone_id":"c275d35a8885de79ca8e61c17ac97ab6","zone_status":"active","zone_type":"full"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"porkbun_nameservers","name":"domains","provider":"provider[\"registry.opentofu.org/marcfrederick/porkbun\"]","instances":[{"index_key":"aprs.me","schema_version":0,"attributes":{"domain":"aprs.me","nameservers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"]},"sensitive_attributes":[]},{"index_key":"beanjar.org","schema_version":0,"attributes":{"domain":"beanjar.org","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"gridmap.org","schema_version":0,"attributes":{"domain":"gridmap.org","nameservers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"]},"sensitive_attributes":[]},{"index_key":"gunincrements.com","schema_version":0,"attributes":{"domain":"gunincrements.com","nameservers":["curitiba.ns.porkbun.com","fortaleza.ns.porkbun.com","maceio.ns.porkbun.com","salvador.ns.porkbun.com"]},"sensitive_attributes":[]},{"index_key":"manero.org","schema_version":0,"attributes":{"domain":"manero.org","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"mcintire.me","schema_version":0,"attributes":{"domain":"mcintire.me","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"ntxarms.com","schema_version":0,"attributes":{"domain":"ntxarms.com","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"qth.social","schema_version":0,"attributes":{"domain":"qth.social","nameservers":["curitiba.ns.porkbun.com","fortaleza.ns.porkbun.com","maceio.ns.porkbun.com","salvador.ns.porkbun.com"]},"sensitive_attributes":[]},{"index_key":"vntx.org","schema_version":0,"attributes":{"domain":"vntx.org","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"w5isp.com","schema_version":0,"attributes":{"domain":"w5isp.com","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]}]}],"check_results":null} +{"version":4,"terraform_version":"1.11.5","serial":25,"lineage":"aada9e42-4c37-f0e0-91fa-f89b3caac095","outputs":{},"resources":[{"mode":"managed","type":"cloudflare_record","name":"aprs_me_tunnel","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"d3d819ba-925c-4d84-8052-68ed32b686bb.cfargotunnel.com","created_on":"2026-02-18T18:34:56.202856Z","data":[],"hostname":"aprs.me","id":"52fe06c580f9bc70a4ff4a6978de8ea7","metadata":{},"modified_on":"2026-02-18T18:34:56.202856Z","name":"aprs.me","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"caa","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"index_key":"aprs.me-letsencrypt.org","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"letsencrypt.org\"","created_on":"2026-02-25T19:20:24.920388Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"letsencrypt.org","weight":0}],"hostname":"aprs.me","id":"e9f4343f7c0dd97351b6868d48ac3ffc","metadata":{},"modified_on":"2026-02-25T19:20:24.920388Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"aprs.me-pki.goog","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"pki.goog\"","created_on":"2026-02-25T19:20:24.931937Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"pki.goog","weight":0}],"hostname":"aprs.me","id":"312b7a4e5ef24aa39d0d644448b42ba1","metadata":{},"modified_on":"2026-02-25T19:20:24.931937Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"aprs.me-sectigo.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"sectigo.com\"","created_on":"2026-02-25T19:20:23.4046Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"sectigo.com","weight":0}],"hostname":"aprs.me","id":"a03268c868921324d93c424b75a66123","metadata":{},"modified_on":"2026-02-25T19:20:23.4046Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"aprs.me-ssl.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"ssl.com\"","created_on":"2026-02-25T19:20:23.757538Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"ssl.com","weight":0}],"hostname":"aprs.me","id":"ca710bf09e7b007ed13e55f6cb3b72ba","metadata":{},"modified_on":"2026-02-25T19:20:23.757538Z","name":"aprs.me","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-letsencrypt.org","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"letsencrypt.org\"","created_on":"2026-02-25T19:20:26.213276Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"letsencrypt.org","weight":0}],"hostname":"gridmap.org","id":"ec4d9e50c72f265010b4dc56db1f21f7","metadata":{},"modified_on":"2026-02-25T19:20:26.213276Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-pki.goog","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"pki.goog\"","created_on":"2026-02-25T19:20:25.34753Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"pki.goog","weight":0}],"hostname":"gridmap.org","id":"bf2e711ac3abb681fc725daa2e0a2c53","metadata":{},"modified_on":"2026-02-25T19:20:25.34753Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-sectigo.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"sectigo.com\"","created_on":"2026-02-25T19:20:26.603772Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"sectigo.com","weight":0}],"hostname":"gridmap.org","id":"8175bf30b3d31b82c4c1b85af252ac7e","metadata":{},"modified_on":"2026-02-25T19:20:26.603772Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"gridmap.org-ssl.com","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 issue \"ssl.com\"","created_on":"2026-02-25T19:20:23.273147Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"0","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":0,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"issue","target":"","type":0,"usage":0,"value":"ssl.com","weight":0}],"hostname":"gridmap.org","id":"03749ab647ee85cdf0bb7ba8bcbba3c1","metadata":{},"modified_on":"2026-02-25T19:20:23.273147Z","name":"gridmap.org","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CAA","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"gridmap_org_tunnel","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"d3d819ba-925c-4d84-8052-68ed32b686bb.cfargotunnel.com","created_on":"2026-02-18T18:34:53.58717Z","data":[],"hostname":"gridmap.org","id":"8b218c8e8e8a78aa8c30a5e82e63dbdc","metadata":{},"modified_on":"2026-02-18T18:34:53.58717Z","name":"gridmap.org","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"records","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"index_key":"towerops.net-a-dfw-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"144.202.64.79","created_on":"2026-02-04T18:34:31.048255Z","data":[],"hostname":"dfw.poller.towerops.net","id":"aa8183b748f1bf0e3850d9dabb91ee3a","metadata":{},"modified_on":"2026-02-04T18:34:31.048255Z","name":"dfw.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-a-lon-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"45.77.228.168","created_on":"2026-02-08T17:23:07.876902Z","data":[],"hostname":"lon.poller.towerops.net","id":"2a25c3ee47284833cea451170d3bd61d","metadata":{},"modified_on":"2026-02-08T17:23:07.876902Z","name":"lon.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-a-staging","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"204.110.191.231","created_on":"2026-02-09T18:23:25.894749Z","data":[],"hostname":"staging.towerops.net","id":"8430e2789d5700c556dc7a598d49d8fd","metadata":{},"modified_on":"2026-02-09T18:44:11.107671Z","name":"staging","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-a-www","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"Created during Cloudflare Rules deployment process for Redirect from WWW to root [Template]","content":"192.0.2.1","created_on":"2026-02-01T16:08:06.434962Z","data":[],"hostname":"www.towerops.net","id":"689c47b0e5ed68427f2c3b9e25bdf602","metadata":{},"modified_on":"2026-02-01T16:08:06.434962Z","name":"www","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"A","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-aaaa-dfw-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"2001:19f0:6401:af2:5400:5ff:fee7:1bfb","created_on":"2026-02-04T18:34:31.174197Z","data":[],"hostname":"dfw.poller.towerops.net","id":"2edc0cf203313e67d64316daf8a0490e","metadata":{},"modified_on":"2026-02-04T18:34:31.174197Z","name":"dfw.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"AAAA","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-aaaa-lon-poller","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"2001:19f0:7400:1195:5400:5ff:feee:a0c3","created_on":"2026-02-08T17:24:02.914425Z","data":[],"hostname":"lon.poller.towerops.net","id":"610f21b8597a1ce6ad4020f15311787a","metadata":{},"modified_on":"2026-02-08T17:24:02.914425Z","name":"lon.poller","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"AAAA","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-autoconfig","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.mcintire.me","created_on":"2026-01-31T21:12:11.834659Z","data":[],"hostname":"autoconfig.towerops.net","id":"1c0f59e7bd583f8971f50231e6fad3cf","metadata":{},"modified_on":"2026-02-18T18:21:20.964531Z","name":"autoconfig","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-autodiscover","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.mcintire.me","created_on":"2026-01-31T21:12:11.813721Z","data":[],"hostname":"autodiscover.towerops.net","id":"34a4a9bde8cf4626635cfa3cb5f8c408","metadata":{},"modified_on":"2026-02-18T18:21:21.959176Z","name":"autodiscover","priority":null,"proxiable":true,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dev","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"db59aa44-79de-4522-9593-43e828f3215a.cfargotunnel.com","created_on":"2026-02-01T16:17:24.029892Z","data":[],"hostname":"dev.towerops.net","id":"21b470e19493c856ce697052dabc90e5","metadata":{},"modified_on":"2026-02-18T18:21:15.242958Z","name":"dev","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dkim-ses-1","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"2skkb53ep3vfbkdstfmm2elfthurggid.dkim.amazonses.com","created_on":"2026-02-06T14:56:00.620182Z","data":[],"hostname":"2skkb53ep3vfbkdstfmm2elfthurggid._domainkey.towerops.net","id":"f4a446531128011a1a6b896010983901","metadata":{},"modified_on":"2026-02-18T18:21:23.474077Z","name":"2skkb53ep3vfbkdstfmm2elfthurggid._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dkim-ses-2","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"4uwsaywjpmk35j36pfiz4vr37dyy4v7z.dkim.amazonses.com","created_on":"2026-02-06T14:56:00.429727Z","data":[],"hostname":"4uwsaywjpmk35j36pfiz4vr37dyy4v7z._domainkey.towerops.net","id":"aefc5c72e25e3656dbccf0fdf2fc6f48","metadata":{},"modified_on":"2026-02-18T18:21:18.993371Z","name":"4uwsaywjpmk35j36pfiz4vr37dyy4v7z._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-dkim-ses-3","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"l7lbifvot6nv3wwho5rs35xzqzzinz5f.dkim.amazonses.com","created_on":"2026-02-06T14:56:00.400622Z","data":[],"hostname":"l7lbifvot6nv3wwho5rs35xzqzzinz5f._domainkey.towerops.net","id":"02a4cfa316dae29898a8270f2c3aceda","metadata":{},"modified_on":"2026-02-18T18:21:15.477573Z","name":"l7lbifvot6nv3wwho5rs35xzqzzinz5f._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-cname-root","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"d3d819ba-925c-4d84-8052-68ed32b686bb.cfargotunnel.com","created_on":"2026-01-31T21:20:07.097359Z","data":[],"hostname":"towerops.net","id":"cf1bc4e745466032786db854f4294f99","metadata":{},"modified_on":"2026-02-18T18:21:17.216154Z","name":"towerops.net","priority":null,"proxiable":true,"proxied":true,"tags":[],"timeouts":null,"ttl":1,"type":"CNAME","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-mx-mail","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"feedback-smtp.us-east-1.amazonses.com","created_on":"2026-01-31T21:12:11.92231Z","data":[],"hostname":"mail.towerops.net","id":"a2486e2bbb4d6aca978d28807893e346","metadata":{},"modified_on":"2026-01-31T21:12:11.92231Z","name":"mail","priority":10,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"MX","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-mx-root-backup","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.nsnw.ca","created_on":"2026-01-31T21:12:11.783987Z","data":[],"hostname":"towerops.net","id":"bbd552d191e61345f19f8dae10281f9e","metadata":{},"modified_on":"2026-01-31T21:12:11.783987Z","name":"towerops.net","priority":20,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"MX","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-mx-root-primary","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"mail.mcintire.me","created_on":"2026-01-31T21:12:11.794166Z","data":[],"hostname":"towerops.net","id":"a5dc7d6dc6d9085132a8fa0b7db1b69a","metadata":{},"modified_on":"2026-01-31T21:12:11.794166Z","name":"towerops.net","priority":10,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"MX","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-dkim","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuFm6nd0L7/nAO4m0MPxdjwRAup5C90DPSPa0TQBDT8S2WvASyQUPV8xSbICGMj6FjZYNd5ph/0VK47gDulxQgNQT96IVf2ooTfkvY/80bAyEdtJbpjEUzQnX5YBi/pJigQx2EeCdaojUZumeIdlFlzeb8QQIEtQhVnyc1Xc/Z8ljPMW1H9DXLSDINjV6UTaXX+PZ7jgpRKgU+RnJNcqTAkhhKQ4bMcRfOam/lF6wPOWte1A7ir08TdE8O67ioqygkr/ivAp7xvV4vGOkY6buLLE+c3exY+my2qTCZgJyUMAhkZzYqzGo1csfHH8x+O2XN/k8Z5jPhq8LSvl9XkllKwIDAQAB","created_on":"2026-01-31T21:12:11.932177Z","data":[],"hostname":"dkim._domainkey.towerops.net","id":"3872e537e8cee4087ead7eb5785e1426","metadata":{},"modified_on":"2026-02-03T19:17:59.246248Z","name":"dkim._domainkey","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-dmarc","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=DMARC1; p=none;","created_on":"2026-01-31T21:12:11.846177Z","data":[],"hostname":"_dmarc.towerops.net","id":"bd236f9a89fc42f45fd7f4b076478880","metadata":{},"modified_on":"2026-02-03T19:17:59.427606Z","name":"_dmarc","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-mail-spf","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=spf1 include:amazonses.com ~all","created_on":"2026-01-31T21:12:11.939949Z","data":[],"hostname":"mail.towerops.net","id":"76b8314432391c6630e9e1b2edfce65d","metadata":{},"modified_on":"2026-02-03T19:18:00.631343Z","name":"mail","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]},{"index_key":"towerops.net-txt-spf","schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"v=spf1 mx a ip4:107.174.178.20 ~all","created_on":"2026-01-31T21:12:11.825178Z","data":[],"hostname":"towerops.net","id":"cf2f74d345960700a81a6025e7a89642","metadata":{},"modified_on":"2026-02-03T19:17:59.830149Z","name":"towerops.net","priority":null,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"TXT","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_record","name":"towerops_srv_autodiscover","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":3,"attributes":{"allow_overwrite":false,"comment":"","content":"0 443 mail.mcintire.me","created_on":"2026-01-31T21:12:11.914868Z","data":[{"algorithm":0,"altitude":0,"certificate":"","content":"","digest":"","digest_type":0,"fingerprint":"","flags":"","key_tag":0,"lat_degrees":0,"lat_direction":"","lat_minutes":0,"lat_seconds":0,"long_degrees":0,"long_direction":"","long_minutes":0,"long_seconds":0,"matching_type":0,"name":"","order":0,"port":443,"precision_horz":0,"precision_vert":0,"preference":0,"priority":0,"proto":"","protocol":0,"public_key":"","regex":"","replacement":"","selector":0,"service":"","size":0,"tag":"","target":"mail.mcintire.me","type":0,"usage":0,"value":"","weight":0}],"hostname":"_autodiscover._tcp.towerops.net","id":"26db78978535d1d630432970b9a4e279","metadata":{},"modified_on":"2026-01-31T21:12:11.914868Z","name":"_autodiscover._tcp","priority":0,"proxiable":false,"proxied":false,"tags":[],"timeouts":null,"ttl":1,"type":"SRV","value":null,"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"private":"eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMCwidXBkYXRlIjozMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMyJ9","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_ruleset","name":"aprs_me_firewall","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":1,"attributes":{"account_id":null,"description":"","id":"9a107a7272a14c05b9e0e747b5a93bf7","kind":"zone","name":"default","phase":"http_request_firewall_custom","rules":[{"action":"block","action_parameters":[],"description":"block abusers","enabled":true,"exposed_credential_check":[],"expression":"(ip.src.country in {\"KP\" \"RU\" \"CN\" \"SY\" \"MM\" \"VE\" \"IQ\" \"LY\" \"SO\" \"YE\" \"SS\"}) or (http.request.uri.path contains \".php\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\") or (http.request.uri.path contains \"wlwmanifest.xml\") or (http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git\") or (http.request.uri.path contains \"/phpmyadmin\") or (http.request.uri.path contains \"/cgi-bin\") or (not http.user_agent ne \"\")","id":"6bbd814f49634e3997386d6fa1193e4e","logging":[],"ratelimit":[],"ref":"6bbd814f49634e3997386d6fa1193e4e"}],"zone_id":"09da59049b0ecad7b734ce3d902bf581"},"sensitive_attributes":[],"dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_ruleset","name":"gridmap_firewall","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":1,"attributes":{"account_id":null,"description":"","id":"7dfc79c24a22439ea501865479822df8","kind":"zone","name":"default","phase":"http_request_firewall_custom","rules":[{"action":"block","action_parameters":[],"description":"block abusers","enabled":true,"exposed_credential_check":[],"expression":"(ip.src.country in {\"KP\" \"RU\" \"CN\" \"SY\" \"MM\" \"VE\" \"IQ\" \"LY\" \"SO\" \"YE\" \"SS\"}) or (http.request.uri.path contains \".php\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\") or (http.request.uri.path contains \"wlwmanifest.xml\") or (http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git\") or (http.request.uri.path contains \"/phpmyadmin\") or (http.request.uri.path contains \"/cgi-bin\") or (not http.user_agent ne \"\")","id":"b90e87338cd84bd4a8caad80851eb539","logging":[],"ratelimit":[],"ref":"b90e87338cd84bd4a8caad80851eb539"}],"zone_id":"836f24d97eb51ffc18aa10c0a98efa48"},"sensitive_attributes":[],"dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_ruleset","name":"towerops_firewall","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":1,"attributes":{"account_id":null,"description":"","id":"3c209e50f21b40388733a7cfe8f62f4e","kind":"zone","name":"default","phase":"http_request_firewall_custom","rules":[{"action":"skip","action_parameters":[{"additional_cacheable_ports":null,"algorithms":[],"automatic_https_rewrites":null,"autominify":[],"bic":null,"browser_ttl":[],"cache":null,"cache_key":[],"cache_reserve":[],"content":null,"content_type":null,"cookie_fields":null,"disable_apps":null,"disable_railgun":null,"disable_rum":null,"disable_zaraz":null,"edge_ttl":[],"email_obfuscation":null,"fonts":null,"from_list":[],"from_value":[],"headers":[],"host_header":null,"hotlink_protection":null,"id":null,"increment":null,"matched_data":[],"mirage":null,"opportunistic_encryption":null,"origin":[],"origin_cache_control":null,"origin_error_page_passthru":null,"overrides":[],"phases":null,"polish":null,"products":null,"read_timeout":null,"request_fields":null,"respect_strong_etags":null,"response":[],"response_fields":null,"rocket_loader":null,"rules":null,"ruleset":"current","rulesets":null,"security_level":null,"serve_stale":[],"server_side_excludes":null,"sni":[],"ssl":null,"status_code":null,"sxg":null,"uri":[]}],"description":"allow agent websocket connections","enabled":true,"exposed_credential_check":[],"expression":"(http.request.uri.path eq \"/socket/agent/websocket\")","id":"9016d5312697421c8687a7389f4a4a4d","logging":[{"enabled":false}],"ratelimit":[],"ref":"9016d5312697421c8687a7389f4a4a4d"},{"action":"block","action_parameters":[],"description":"block abusers","enabled":true,"exposed_credential_check":[],"expression":"(ip.src.country in {\"KP\" \"RU\" \"CN\" \"SY\" \"MM\" \"VE\" \"IQ\" \"LY\" \"SO\" \"YE\" \"SS\"}) or (http.request.uri.path contains \".php\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\") or (http.request.uri.path contains \"wlwmanifest.xml\") or (http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git\") or (http.request.uri.path contains \"/phpmyadmin\") or (http.request.uri.path contains \"/cgi-bin\") or (not http.user_agent ne \"\")","id":"2c5dccdb7aed4c18aaf8532fd19275eb","logging":[],"ratelimit":[],"ref":"2c5dccdb7aed4c18aaf8532fd19275eb"}],"zone_id":"c275d35a8885de79ca8e61c17ac97ab6"},"sensitive_attributes":[],"dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_zero_trust_tunnel_cloudflared_config","name":"towerops","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","config":[{"ingress_rule":[{"hostname":"aprs.me","origin_request":[],"path":"","service":"http://aprs.aprs.svc.cluster.local:4000"},{"hostname":"gridmap.org","origin_request":[],"path":"","service":"http://gridmap.gridmap.svc.cluster.local:4000"},{"hostname":"towerops.net","origin_request":[],"path":"","service":"http://towerops.towerops.svc.cluster.local:4000"},{"hostname":"","origin_request":[],"path":"","service":"http_status:404"}],"origin_request":[],"warp_routing":[]}],"id":"d3d819ba-925c-4d84-8052-68ed32b686bb","tunnel_id":"d3d819ba-925c-4d84-8052-68ed32b686bb"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="}]},{"mode":"managed","type":"cloudflare_zero_trust_tunnel_cloudflared_config","name":"towerops_dev","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","config":[{"ingress_rule":[{"hostname":"dev.towerops.net","origin_request":[],"path":"","service":"http://localhost:4000"},{"hostname":"","origin_request":[],"path":"","service":"http_status:404"}],"origin_request":[],"warp_routing":[]}],"id":"db59aa44-79de-4522-9593-43e828f3215a","tunnel_id":"db59aa44-79de-4522-9593-43e828f3215a"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="}]},{"mode":"managed","type":"cloudflare_zone","name":"zones","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"index_key":"aprs.me","schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","id":"09da59049b0ecad7b734ce3d902bf581","jump_start":null,"meta":{"phishing_detected":false,"wildcard_proxiable":false},"name_servers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"],"paused":false,"plan":"free","status":"active","type":"full","vanity_name_servers":[],"verification_key":"","zone":"aprs.me"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="},{"index_key":"gridmap.org","schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","id":"836f24d97eb51ffc18aa10c0a98efa48","jump_start":null,"meta":{"phishing_detected":false,"wildcard_proxiable":false},"name_servers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"],"paused":false,"plan":"free","status":"active","type":"full","vanity_name_servers":[],"verification_key":"","zone":"gridmap.org"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="},{"index_key":"towerops.net","schema_version":0,"attributes":{"account_id":"4d18148a82b0a91f289afbbd2944a603","id":"c275d35a8885de79ca8e61c17ac97ab6","jump_start":null,"meta":{"phishing_detected":false,"wildcard_proxiable":false},"name_servers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"],"paused":false,"plan":"free","status":"active","type":"full","vanity_name_servers":[],"verification_key":"","zone":"towerops.net"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="}]},{"mode":"managed","type":"cloudflare_zone_settings_override","name":"aprs_me","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":2,"attributes":{"id":"09da59049b0ecad7b734ce3d902bf581","initial_settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"off","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.0","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":false,"include_subdomains":false,"max_age":0,"nosniff":false,"preload":false}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"on","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"off","websockets":"on","zero_rtt":"off"}],"initial_settings_read_at":"2026-02-25T19:03:59.962087Z","readonly_settings":["advanced_ddos","http2","long_lived_grpc","mirage","origin_error_page_pass_thru","polish","prefetch_preload","proxy_read_timeout","response_buffering","sort_query_string_for_cache","true_client_ip_header","webp","image_resizing"],"settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":true,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"strict","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"","websockets":"on","zero_rtt":"on"}],"zone_id":"09da59049b0ecad7b734ce3d902bf581","zone_status":"active","zone_type":"full"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_zone_settings_override","name":"gridmap_org","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":2,"attributes":{"id":"836f24d97eb51ffc18aa10c0a98efa48","initial_settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"off","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.0","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":false,"include_subdomains":false,"max_age":0,"nosniff":false,"preload":false}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"on","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"off","websockets":"on","zero_rtt":"off"}],"initial_settings_read_at":"2026-02-25T19:04:25.78697Z","readonly_settings":["advanced_ddos","http2","long_lived_grpc","mirage","origin_error_page_pass_thru","polish","prefetch_preload","proxy_read_timeout","response_buffering","sort_query_string_for_cache","true_client_ip_header","webp","image_resizing"],"settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"off","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":true,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"strict","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"","websockets":"on","zero_rtt":"on"}],"zone_id":"836f24d97eb51ffc18aa10c0a98efa48","zone_status":"active","zone_type":"full"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"cloudflare_zone_settings_override","name":"towerops_net","provider":"provider[\"registry.opentofu.org/cloudflare/cloudflare\"]","instances":[{"schema_version":2,"attributes":{"id":"c275d35a8885de79ca8e61c17ac97ab6","initial_settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"add_header","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":false,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"off","websockets":"on","zero_rtt":"on"}],"initial_settings_read_at":"2026-02-25T19:04:04.422661Z","readonly_settings":["advanced_ddos","http2","long_lived_grpc","mirage","origin_error_page_pass_thru","polish","prefetch_preload","proxy_read_timeout","response_buffering","sort_query_string_for_cache","true_client_ip_header","webp","image_resizing"],"settings":[{"aegis":[{"enabled":false,"pool_id":""}],"always_online":"off","always_use_https":"on","automatic_https_rewrites":"on","binary_ast":"off","brotli":"on","browser_cache_ttl":14400,"browser_check":"on","cache_level":"aggressive","challenge_ttl":1800,"ciphers":[],"cname_flattening":"flatten_at_root","development_mode":"off","early_hints":"off","email_obfuscation":"on","filter_logs_to_cloudflare":"off","fonts":"off","h2_prioritization":"off","hotlink_protection":"off","http2":"on","http3":"on","image_resizing":"off","ip_geolocation":"on","ipv6":"on","log_to_cloudflare":"on","max_upload":100,"min_tls_version":"1.2","minify":[],"mirage":"off","mobile_redirect":[],"nel":[{"enabled":true}],"opportunistic_encryption":"on","opportunistic_onion":"on","orange_to_orange":"off","origin_error_page_pass_thru":"off","origin_max_http_version":"2","polish":"off","prefetch_preload":"off","privacy_pass":"on","proxy_read_timeout":"100","pseudo_ipv4":"add_header","replace_insecure_js":"on","response_buffering":"off","rocket_loader":"off","security_header":[{"enabled":true,"include_subdomains":true,"max_age":15552000,"nosniff":true,"preload":true}],"security_level":"medium","server_side_exclude":"on","sort_query_string_for_cache":"off","speed_brain":"off","ssl":"full","ssl_automatic_mode":"custom","tls_1_2_only":"off","tls_1_3":"zrt","tls_client_auth":"off","true_client_ip_header":"off","universal_ssl":"","visitor_ip":"on","waf":"off","webp":"","websockets":"on","zero_rtt":"on"}],"zone_id":"c275d35a8885de79ca8e61c17ac97ab6","zone_status":"active","zone_type":"full"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==","dependencies":["cloudflare_zone.zones"]}]},{"mode":"managed","type":"porkbun_nameservers","name":"domains","provider":"provider[\"registry.opentofu.org/marcfrederick/porkbun\"]","instances":[{"index_key":"aprs.me","schema_version":0,"attributes":{"domain":"aprs.me","nameservers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"]},"sensitive_attributes":[]},{"index_key":"beanjar.org","schema_version":0,"attributes":{"domain":"beanjar.org","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"gridmap.org","schema_version":0,"attributes":{"domain":"gridmap.org","nameservers":["arch.ns.cloudflare.com","tani.ns.cloudflare.com"]},"sensitive_attributes":[]},{"index_key":"gunincrements.com","schema_version":0,"attributes":{"domain":"gunincrements.com","nameservers":["curitiba.ns.porkbun.com","fortaleza.ns.porkbun.com","maceio.ns.porkbun.com","salvador.ns.porkbun.com"]},"sensitive_attributes":[]},{"index_key":"manero.org","schema_version":0,"attributes":{"domain":"manero.org","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"mcintire.me","schema_version":0,"attributes":{"domain":"mcintire.me","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"ntxarms.com","schema_version":0,"attributes":{"domain":"ntxarms.com","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"qth.social","schema_version":0,"attributes":{"domain":"qth.social","nameservers":["curitiba.ns.porkbun.com","fortaleza.ns.porkbun.com","maceio.ns.porkbun.com","salvador.ns.porkbun.com"]},"sensitive_attributes":[]},{"index_key":"vntx.org","schema_version":0,"attributes":{"domain":"vntx.org","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]},{"index_key":"w5isp.com","schema_version":0,"attributes":{"domain":"w5isp.com","nameservers":["ns-global.kjsl.com","ns1.as393837.net","ns2.as393837.net"]},"sensitive_attributes":[]}]}],"check_results":null}