From d867522fbe9b3fb6a69454d4c978ec03a2f4c595 Mon Sep 17 00:00:00 2001 From: Jim Date: Sat, 15 Jun 2024 10:09:21 -0700 Subject: [PATCH] Initialize newly refactored infra repo. --- .gitignore | 11 + ansible/README.md | 15 ++ ansible/ansible.cfg | 11 + ansible/group_vars/all/caddy.yml | 8 + ansible/group_vars/all/node_exporter.yml | 11 + ansible/group_vars/all/sysadmin.yml | 6 + ansible/group_vars/all/versions.yml | 32 +++ ansible/group_vars/mail/disposable-mail.yml | 13 + .../group_vars/observability/alertmanager.yml | 14 ++ .../observability/blackbox_exporter.yml | 32 +++ ansible/group_vars/observability/grafana.yml | 46 ++++ ansible/group_vars/observability/ntfy.yml | 7 + .../group_vars/observability/prometheus.yml | 18 ++ .../gabite.bitcoiner.social/certbot.yml | 12 + .../disposable-mail.yml | 2 + .../gabite.bitcoiner.social/linux.yml | 4 + .../gabite.bitcoiner.social/strfry.yml | 50 ++++ .../garchomp.bitcoiner.social/acme-lego.yml | 11 + .../garchomp.bitcoiner.social/caddy.yml | 42 ++++ .../disposable-mail.yml | 2 + .../garchomp.bitcoiner.social/doas.yml | 5 + .../garchomp.bitcoiner.social/nodejs.yml | 3 + .../garchomp.bitcoiner.social/snort.yml | 6 + .../garchomp.bitcoiner.social/strfry.yml | 50 ++++ ansible/hosts.cfg | 16 ++ ansible/playbooks/caddy.yml | 5 + .../playbooks/group_tasks/certbot/main.yml | 40 +++ .../group_tasks/certbot/tasks/dhparams.yml | 22 ++ .../group_tasks/certbot/tasks/nginx_conf.yml | 70 ++++++ .../certbot/tasks/node_exporter.yml | 26 ++ ansible/playbooks/group_tasks/lego.yml | 28 +++ ansible/playbooks/group_tasks/mail.yml | 25 ++ ansible/playbooks/group_tasks/nginx.yml | 7 + .../playbooks/group_tasks/observability.yml | 24 ++ ansible/playbooks/group_tasks/postgresql.yml | 22 ++ ansible/playbooks/group_tasks/strfry.yml | 5 + ansible/playbooks/group_tasks/wireguard.yml | 7 + .../gabite.bitcoiner.social/README.md | 9 + .../files/nginx/bitcoiner.social.conf | 94 ++++++++ .../files/nginx/nostr.bitcoiner.social.conf | 33 +++ .../files/nginx/tor_bitcoiner.social.conf | 30 +++ .../gabite.bitcoiner.social/files/nip11.json | 1 + .../gabite.bitcoiner.social/files/nip5.json | 1 + .../files/scripts/1984.py | 181 ++++++++++++++ .../files/scripts/compact-strfry-database.sh | 9 + .../files/scripts/prune.ts | 68 ++++++ .../files/scripts/pruning.sh | 16 ++ .../systemd/compact-strfry-database.service | 9 + .../systemd/compact-strfry-database.timer | 9 + .../systemd/prune-strfry-database.service | 11 + .../files/systemd/prune-strfry-database.timer | 9 + .../gabite.bitcoiner.social/main.yml | 85 +++++++ .../gabite.bitcoiner.social/nginx_conf.yml | 90 +++++++ .../files/hablanews.service | 13 + .../garchomp.bitcoiner.social/hablanews.yml | 79 ++++++ .../garchomp.bitcoiner.social/nginx.yml | 146 +++++++++++ .../garchomp.bitcoiner.social/strfry.yml | 8 + .../strfry/logrotate.yml | 18 ++ .../strfry/nginx_conf.yml | 228 ++++++++++++++++++ .../strfry/templates/snort_redirect.conf.j2 | 14 ++ ansible/playbooks/linux.yml | 8 + ansible/playbooks/main.yml | 16 ++ ansible/playbooks/node_exporter.yml | 7 + ansible/playbooks/oneshots/README.md | 11 + ansible/playbooks/oneshots/doas/main.yml | 40 +++ .../oneshots/doas/templates/doas.conf.j2 | 5 + ansible/playbooks/ssh.yml | 40 +++ ansible/playbooks/tor.yml | 8 + ansible/requirements.txt | 3 + ansible/requirements.yml | 78 ++++++ dnscontrol/creds.json | 10 + dnscontrol/dnsconfig.js | 24 ++ docs/decisions.md | 24 ++ scripts/backups/batch.sh | 16 ++ scripts/backups/functions.sh | 45 ++++ .../backups/hosts/gabite.bitcoiner.social.sh | 23 ++ .../hosts/garchomp.bitcoiner.social.sh | 14 ++ 77 files changed, 2241 insertions(+) create mode 100644 .gitignore create mode 100644 ansible/README.md create mode 100644 ansible/ansible.cfg create mode 100644 ansible/group_vars/all/caddy.yml create mode 100644 ansible/group_vars/all/node_exporter.yml create mode 100644 ansible/group_vars/all/sysadmin.yml create mode 100644 ansible/group_vars/all/versions.yml create mode 100644 ansible/group_vars/mail/disposable-mail.yml create mode 100644 ansible/group_vars/observability/alertmanager.yml create mode 100644 ansible/group_vars/observability/blackbox_exporter.yml create mode 100644 ansible/group_vars/observability/grafana.yml create mode 100644 ansible/group_vars/observability/ntfy.yml create mode 100644 ansible/group_vars/observability/prometheus.yml create mode 100644 ansible/host_vars/gabite.bitcoiner.social/certbot.yml create mode 100644 ansible/host_vars/gabite.bitcoiner.social/disposable-mail.yml create mode 100644 ansible/host_vars/gabite.bitcoiner.social/linux.yml create mode 100644 ansible/host_vars/gabite.bitcoiner.social/strfry.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/acme-lego.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/caddy.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/disposable-mail.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/doas.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/nodejs.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/snort.yml create mode 100644 ansible/host_vars/garchomp.bitcoiner.social/strfry.yml create mode 100644 ansible/hosts.cfg create mode 100644 ansible/playbooks/caddy.yml create mode 100644 ansible/playbooks/group_tasks/certbot/main.yml create mode 100644 ansible/playbooks/group_tasks/certbot/tasks/dhparams.yml create mode 100644 ansible/playbooks/group_tasks/certbot/tasks/nginx_conf.yml create mode 100644 ansible/playbooks/group_tasks/certbot/tasks/node_exporter.yml create mode 100644 ansible/playbooks/group_tasks/lego.yml create mode 100644 ansible/playbooks/group_tasks/mail.yml create mode 100644 ansible/playbooks/group_tasks/nginx.yml create mode 100644 ansible/playbooks/group_tasks/observability.yml create mode 100644 ansible/playbooks/group_tasks/postgresql.yml create mode 100644 ansible/playbooks/group_tasks/strfry.yml create mode 100644 ansible/playbooks/group_tasks/wireguard.yml create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/README.md create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/bitcoiner.social.conf create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/nostr.bitcoiner.social.conf create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/tor_bitcoiner.social.conf create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip11.json create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip5.json create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/1984.py create mode 100755 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/compact-strfry-database.sh create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/prune.ts create mode 100755 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/pruning.sh create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.service create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.timer create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.service create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.timer create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/main.yml create mode 100644 ansible/playbooks/host_tasks/gabite.bitcoiner.social/nginx_conf.yml create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/files/hablanews.service create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/hablanews.yml create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/nginx.yml create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry.yml create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/logrotate.yml create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/nginx_conf.yml create mode 100644 ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/templates/snort_redirect.conf.j2 create mode 100644 ansible/playbooks/linux.yml create mode 100644 ansible/playbooks/main.yml create mode 100644 ansible/playbooks/node_exporter.yml create mode 100644 ansible/playbooks/oneshots/README.md create mode 100644 ansible/playbooks/oneshots/doas/main.yml create mode 100644 ansible/playbooks/oneshots/doas/templates/doas.conf.j2 create mode 100644 ansible/playbooks/ssh.yml create mode 100644 ansible/playbooks/tor.yml create mode 100644 ansible/requirements.txt create mode 100644 ansible/requirements.yml create mode 100644 dnscontrol/creds.json create mode 100644 dnscontrol/dnsconfig.js create mode 100644 docs/decisions.md create mode 100755 scripts/backups/batch.sh create mode 100644 scripts/backups/functions.sh create mode 100755 scripts/backups/hosts/gabite.bitcoiner.social.sh create mode 100755 scripts/backups/hosts/garchomp.bitcoiner.social.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dc22cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +ansible/collections +ansible/roles +archive +dhparams.pem +imap.passwd +self-signed-ca +external-requirements-only.yml +ansible/playbooks/test.yml +gabite.bitcoiner.social/files/scripts +strfry-policies diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..3a691d4 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,15 @@ +# Ansible Infrastructure + +Remember to update collections and roles periodically. + +```bash +ansible-galaxy install -r requirements.yml --ignore-errors --force +``` + +## NixOS note + +Workaround to run with `jmespath` on NixOS (for caddy and grafana which need it): + +```shell +nix-shell -p python311Packages.jmespath --run "ansible-playbook playbooks/group_tasks/observability/main.yml" +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..df7907c --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,11 @@ +[defaults] +inventory = hosts.cfg +verbose=1 +roles_path = roles +collections_path = collections + +[ssh_connection] +pipelining=True + +[inventory] +enable_plugins = yaml, ini diff --git a/ansible/group_vars/all/caddy.yml b/ansible/group_vars/all/caddy.yml new file mode 100644 index 0000000..8c4bc86 --- /dev/null +++ b/ansible/group_vars/all/caddy.yml @@ -0,0 +1,8 @@ +--- +caddy_home: /var/lib/caddy +caddy_systemd_capabilities_enabled: yes +caddy_config: | + # node_exporter + {{ inventory_hostname }}:4430 { + reverse_proxy http://127.0.0.1:8030 + } diff --git a/ansible/group_vars/all/node_exporter.yml b/ansible/group_vars/all/node_exporter.yml new file mode 100644 index 0000000..25b8b51 --- /dev/null +++ b/ansible/group_vars/all/node_exporter.yml @@ -0,0 +1,11 @@ +--- # prometheus.prometheus.node_exporter role variables +# https://prometheus-community.github.io/ansible/branch/main/node_exporter_role.html + +node_exporter_web_listen_address: "127.0.0.1:8030" +node_exporter_enabled_collectors: # https://github.com/prometheus/node_exporter#collectors + - cpu.info # CPU Model + - interrupts + - netstat + - vmstat + - systemd + - processes diff --git a/ansible/group_vars/all/sysadmin.yml b/ansible/group_vars/all/sysadmin.yml new file mode 100644 index 0000000..07c76e2 --- /dev/null +++ b/ansible/group_vars/all/sysadmin.yml @@ -0,0 +1,6 @@ +--- # This variable is used by: +# playbooks/podman.yml +# playbooks/ssh.yml +# roles/bleetube.dotfiles + +sysadmin_username: jim \ No newline at end of file diff --git a/ansible/group_vars/all/versions.yml b/ansible/group_vars/all/versions.yml new file mode 100644 index 0000000..3bbc27e --- /dev/null +++ b/ansible/group_vars/all/versions.yml @@ -0,0 +1,32 @@ +--- +# https://github.com/hoytech/strfry/tags +strfry_version: master + +# https://git.v0l.io/Kieran/snort/releases +#snort_version: v0.1.20 # We must merge our branch for updates, the role follows a branch + +# https://github.com/prometheus/node_exporter/releases +node_exporter_version: latest + +# https://github.com/prometheus/prometheus/releases +prometheus_version: latest + +# https://github.com/prometheus/blackbox_exporter/releases +blackbox_exporter_version: '0.25.0' + +# https://github.com/prometheus/alertmanager/releases +alertmanager_version: latest + +# https://github.com/grafana/grafana/releases +#grafana_version: latest # collection writes deprecated alerting config +grafana_version: '10.4.3' + +# https://git.xenrox.net/~xenrox/ntfy-alertmanager +ntfy_alertmanager_version: v0.3.0 + +# https://go.dev/dl/ +# https://github.com/gantsign/ansible-role-golang/tree/master/vars/versions +#golang_version: "1.22.2" # gantsign.golang, default should be latest + +# https://grafana.com/grafana/dashboards/1860-node-exporter-full/?tab=revisions +grafana_node_dashboard_version: 36 diff --git a/ansible/group_vars/mail/disposable-mail.yml b/ansible/group_vars/mail/disposable-mail.yml new file mode 100644 index 0000000..492089a --- /dev/null +++ b/ansible/group_vars/mail/disposable-mail.yml @@ -0,0 +1,13 @@ +--- # bleetube.disposable-mail role variables +dkim_selector: mail +dkim_key_path: /etc/dkimkeys +postfix_hostname: "{{ dkim_selector }}.{{ postfix_domain }}" +imap_bind_address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}" +postfix_smtpd_tls_cert_file: "/var/acme/certificates/{{ postfix_hostname }}.crt" +postfix_smtpd_tls_key_file: "/var/acme/certificates/{{ postfix_hostname }}.key" +postfix_smtpd_tls_dh1024_param_file: /etc/ssl/dhparams.pem +postfix_maildir_user: maildir +postfix_inet_interfaces: all +postfix_virtual_mailbox_base: /var/vmail +postfix_mynetworks: + - 127.0.0.0/8 \ No newline at end of file diff --git a/ansible/group_vars/observability/alertmanager.yml b/ansible/group_vars/observability/alertmanager.yml new file mode 100644 index 0000000..7019229 --- /dev/null +++ b/ansible/group_vars/observability/alertmanager.yml @@ -0,0 +1,14 @@ +--- # prometheus.prometheus.alertmanager role variables +alertmanager_web_listen_address: "127.0.0.1:9093" +alertmanager_web_external_url: "https://{{ inventory_hostname }}/alertmanager/" + +# The overlapping yaml templating requires us to use {% raw %} and {% endraw %}. +alertmanager_receivers: + - name: ntfy + webhook_configs: + - url: "{{ ntfy_alertmanager_base_url }}/{{ ntfy_alertmanager_topic_name }}" + +# https://prometheus.io/docs/alerting/latest/configuration/#route +alertmanager_route: + group_by: [ 'instance'] + receiver: ntfy \ No newline at end of file diff --git a/ansible/group_vars/observability/blackbox_exporter.yml b/ansible/group_vars/observability/blackbox_exporter.yml new file mode 100644 index 0000000..b2b059e --- /dev/null +++ b/ansible/group_vars/observability/blackbox_exporter.yml @@ -0,0 +1,32 @@ +--- # prometheus.prometheus.blacbox_exporter role variables +# https://github.com/prometheus/blackbox_exporter +blackbox_exporter_web_listen_address: "127.0.0.1:8031" + +blackbox_exporter_configuration_modules: + http_2xx: + prober: http + timeout: 5s + http: + valid_status_codes: [] + method: GET + preferred_ip_protocol: ip4 + fail_if_not_ssl: true + + tcp_probe: + prober: tcp + timeout: 5s + + smtp_starttls: + prober: tcp + timeout: 5s + tcp: + query_response: + - expect: "^220 ([^ ]+) ESMTP (.+)$" + - send: "EHLO prober\r" + - expect: "^250-STARTTLS" + - send: "STARTTLS\r" + - expect: "^220" + - starttls: true + - send: "EHLO prober\r" + - expect: "^250-AUTH" + - send: "QUIT\r" diff --git a/ansible/group_vars/observability/grafana.yml b/ansible/group_vars/observability/grafana.yml new file mode 100644 index 0000000..55e8513 --- /dev/null +++ b/ansible/group_vars/observability/grafana.yml @@ -0,0 +1,46 @@ +--- # grafana.grafana.grafana role variables +# https://github.com/grafana/grafana-ansible-collection +# https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ + +grafana_url: "https://{{ inventory_hostname }}" # required for oauth callbacks +grafana_port: 8043 +#grafana_database: +# type: postgres # This only really makes sense if postgres is already in use on this sytem. Otherwise, sqlite is fine. +# host: /var/run/postgresql +# name: grafana +# user: grafana +# password: "{{ lookup('ansible.builtin.env', 'GRAFANA_POSTGRES_PASSWORD') }}" +# max_idle_conn: 2 +# max_open_conn: "" +# log_queries: "" +grafana_analytics: + reporting_enabled: false +grafana_snapshots: + external_enabled: false +grafana_users: + allow_sign_up: false +grafana_datasources: + - name: "{{ inventory_hostname }}" + type: "prometheus" + # Note that this depends on the prometheus_web_external_url variable in ansible/sableye/install-prometheus.yml. + url: "http://localhost:9090/prometheus" + access: "proxy" # "server" in the UI +# basicAuth: true +# basicAuthUser: "admin" +# basicAuthPassword: "password" + isDefault: true + jsonData: + tlsAuth: false + tlsAuthWithCACert: false + tlsSkipVerify: true + +grafana_dashboards: + - dashboard_id: 1860 # https://grafana.com/grafana/dashboards/1860-node-exporter-full/ + revision_id: "{{ grafana_node_dashboard_version|default(31) }}" + datasource: "{{ inventory_hostname }}" + +# grafana_smtp: +# host: +# user: +# password: +# from_address: \ No newline at end of file diff --git a/ansible/group_vars/observability/ntfy.yml b/ansible/group_vars/observability/ntfy.yml new file mode 100644 index 0000000..1c9dfb2 --- /dev/null +++ b/ansible/group_vars/observability/ntfy.yml @@ -0,0 +1,7 @@ +--- # bleetube.ntfy and bleetube.ntfy-alertmanager role variables +ntfy_alertmanager_base_url: "https://{{ inventory_hostname }}:4433" +ntfy_alertmanager_http_address: "127.0.0.1:8033" +#ntfy_web_root: /ntfy + +# fix gantsign.golang bug +golang_download_dir: "{{ x_ansible_download_dir | default(ansible_env.HOME + '/.ansible/tmp/downloads') }}" \ No newline at end of file diff --git a/ansible/group_vars/observability/prometheus.yml b/ansible/group_vars/observability/prometheus.yml new file mode 100644 index 0000000..d7fcadd --- /dev/null +++ b/ansible/group_vars/observability/prometheus.yml @@ -0,0 +1,18 @@ +--- # prometheus.prometheus.prometheus role variables +prometheus_web_listen_address: "127.0.0.1:9090" +# https://www.robustperception.io/using-external-urls-and-proxies-with-prometheus/ +# Note that the grafana_datasources variable also needs to reflect this path. +prometheus_web_external_url: "https://{{ inventory_hostname }}/prometheus/" +# https://prometheus.io/docs/prometheus/latest/storage/#operational-aspects +prometheus_storage_retention: 365d +prometheus_storage_retention_size: 10GB +#prometheus_config_flags_extra: +# storage.tsdb.wal-compression: # enabled by default + +prometheus_alertmanager_config: + - path_prefix: alertmanager/ + static_configs: + - targets: [ "127.0.0.1:9093" ] + +prometheus_alert_rules_files: + - "/etc/prometheus/rules/*.rules" \ No newline at end of file diff --git a/ansible/host_vars/gabite.bitcoiner.social/certbot.yml b/ansible/host_vars/gabite.bitcoiner.social/certbot.yml new file mode 100644 index 0000000..c64c866 --- /dev/null +++ b/ansible/host_vars/gabite.bitcoiner.social/certbot.yml @@ -0,0 +1,12 @@ +--- +certbot_certs: + - domains: + - "{{ inventory_hostname }}" +# - bitcoiner.social +# - nostr.bitcoiner.social + + webroot: /var/www/html +certbot_admin_email: sysadmin.aghast522@passinbox.com +certbot_auto_renew_hour: "1" +certbot_auto_renew_minute: "13" +certbot_create_extra_args: "" \ No newline at end of file diff --git a/ansible/host_vars/gabite.bitcoiner.social/disposable-mail.yml b/ansible/host_vars/gabite.bitcoiner.social/disposable-mail.yml new file mode 100644 index 0000000..e027e7a --- /dev/null +++ b/ansible/host_vars/gabite.bitcoiner.social/disposable-mail.yml @@ -0,0 +1,2 @@ +--- # bleetube.disposable-mail role variables +postfix_domain: bitcoiner.social \ No newline at end of file diff --git a/ansible/host_vars/gabite.bitcoiner.social/linux.yml b/ansible/host_vars/gabite.bitcoiner.social/linux.yml new file mode 100644 index 0000000..08d821a --- /dev/null +++ b/ansible/host_vars/gabite.bitcoiner.social/linux.yml @@ -0,0 +1,4 @@ +--- +sysadmin_packages_custom: + - wireguard + - wireguard-tools diff --git a/ansible/host_vars/gabite.bitcoiner.social/strfry.yml b/ansible/host_vars/gabite.bitcoiner.social/strfry.yml new file mode 100644 index 0000000..e970b5e --- /dev/null +++ b/ansible/host_vars/gabite.bitcoiner.social/strfry.yml @@ -0,0 +1,50 @@ +--- +nginx_strfry_https_port: 443 +nginx_strfry_metrics_port: 4434 +strfry_metrics_plugin_port: 8034 +nginx_strfry_domain: bitcoiner.social +strfry_policies_enabled: yes +strfry_relay: + bind: "127.0.0.1" + port: 8000 + nofiles: 65536 + realIpHeader: x-forwarded-for + info: + name: bitcoiner.social + description: A fast, reliable, and up-to-date nostr relay with monitored server availability and nightly off-site backups. + pubkey: ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b + contact: nostr@bitcoiner.social + maxWebsocketPayloadSize: 131072 + autoPingSeconds: 55 + enableTcpKeepalive: no + queryTimesliceBudgetMicroseconds: 10000 + maxFilterLimit: 500 + maxSubsPerConnection: 20 + writePolicy: + plugin: /var/lib/strfry/complex-entrypoint.ts + lookbackSeconds: 0 + compression: + enabled: yes + slidingWindow: yes + logging: + dumpInAll: no + dumpInEvents: no + dumpInReqs: no + dbScanPerf: no + invalidEvents: yes + numThreads: + ingester: 3 + reqWorker: 3 + reqMonitor: 3 + negentropy: 2 + negentropy: + enabled: yes + maxSyncEvents: 1000000 +strfry_events: + maxEventSize: 65536 + rejectEventsNewerThanSeconds: 900 + rejectEventsOlderThanSeconds: 94608000 + rejectEphemeralEventsOlderThanSeconds: 60 + ephemeralEventsLifetimeSeconds: 300 + maxNumTags: 2000 + maxTagValSize: 1024 \ No newline at end of file diff --git a/ansible/host_vars/garchomp.bitcoiner.social/acme-lego.yml b/ansible/host_vars/garchomp.bitcoiner.social/acme-lego.yml new file mode 100644 index 0000000..0fe87c0 --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/acme-lego.yml @@ -0,0 +1,11 @@ +--- +# prefer lego because synapse uses a version of python3-zope.interface that conflicts with certbot +acme_email: acme@bitcoiner.social +acme_domains: + - { domain: "{{ inventory_hostname }}", provider: namecheap } + - { domain: bitcoiner.social, provider: namecheap } + - { domain: mail.bitcoiner.social, provider: namecheap } + - { domain: nostr.bitcoiner.social, provider: namecheap } + - { domain: snort.bitcoiner.social, provider: namecheap } + - { domain: cast.bitcoiner.social, provider: namecheap } + - { domain: news.bitcoiner.social, provider: namecheap } diff --git a/ansible/host_vars/garchomp.bitcoiner.social/caddy.yml b/ansible/host_vars/garchomp.bitcoiner.social/caddy.yml new file mode 100644 index 0000000..abf07ef --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/caddy.yml @@ -0,0 +1,42 @@ +--- +caddy_systemd_capabilities_enabled: yes +caddy_config: | + # node_exporter + {{ inventory_hostname }}:4430 { + reverse_proxy http://127.0.0.1:8030 + } + + # observability + garchomp.bitcoiner.social { + + # grafana + reverse_proxy http://127.0.0.1:8043 + handle /api/live { + reverse_proxy http://127.0.0.1:8043 + } + handle /public/* { + root * /usr/share/grafana + file_server + } + + # prometheus + handle /prometheus/* { + reverse_proxy http://127.0.0.1:9090 + #header_up Host {host} + } + + # alertmanager + handle /alertmanager/* { + reverse_proxy http://127.0.0.1:9093 + } + } + + # blackbox_exporter + garchomp.bitcoiner.social:4431 { + reverse_proxy http://127.0.0.1:8031 + } + + # ntfy-alertmanager + garchomp.bitcoiner.social:4433 { + reverse_proxy http://127.0.0.1:8033 + } diff --git a/ansible/host_vars/garchomp.bitcoiner.social/disposable-mail.yml b/ansible/host_vars/garchomp.bitcoiner.social/disposable-mail.yml new file mode 100644 index 0000000..e027e7a --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/disposable-mail.yml @@ -0,0 +1,2 @@ +--- # bleetube.disposable-mail role variables +postfix_domain: bitcoiner.social \ No newline at end of file diff --git a/ansible/host_vars/garchomp.bitcoiner.social/doas.yml b/ansible/host_vars/garchomp.bitcoiner.social/doas.yml new file mode 100644 index 0000000..572a727 --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/doas.yml @@ -0,0 +1,5 @@ +--- +# permit nopass blee as postgres +doas_permit_list: + - username: blee + role: opendkim \ No newline at end of file diff --git a/ansible/host_vars/garchomp.bitcoiner.social/nodejs.yml b/ansible/host_vars/garchomp.bitcoiner.social/nodejs.yml new file mode 100644 index 0000000..ee3f6c0 --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/nodejs.yml @@ -0,0 +1,3 @@ +# habla.news requirements +npm_packages: + - pnpm \ No newline at end of file diff --git a/ansible/host_vars/garchomp.bitcoiner.social/snort.yml b/ansible/host_vars/garchomp.bitcoiner.social/snort.yml new file mode 100644 index 0000000..c93c6a3 --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/snort.yml @@ -0,0 +1,6 @@ +--- +snort_repository_url: https://github.com/bleetube/snort.git +snort_version: bitcoiner.social +nginx_snort_domain: snort.bitcoiner.social +nginx_snort_port: 443 +#snort_always_build: yes \ No newline at end of file diff --git a/ansible/host_vars/garchomp.bitcoiner.social/strfry.yml b/ansible/host_vars/garchomp.bitcoiner.social/strfry.yml new file mode 100644 index 0000000..b2a47ca --- /dev/null +++ b/ansible/host_vars/garchomp.bitcoiner.social/strfry.yml @@ -0,0 +1,50 @@ +--- +nginx_strfry_https_port: 443 +nginx_strfry_metrics_port: 4434 +strfry_metrics_plugin_port: 8034 +nginx_strfry_domain: bitcoiner.social +strfry_policies_enabled: no +strfry_relay: + bind: "127.0.0.1" + port: 8030 + nofiles: 65536 + realIpHeader: x-forwarded-for + info: + name: bitcoiner.social + description: A fast, reliable, and up-to-date nostr relay with monitored server availability and nightly off-site backups. + pubkey: ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b + contact: nostr@bitcoiner.social + maxWebsocketPayloadSize: 131072 + autoPingSeconds: 55 + enableTcpKeepalive: no + queryTimesliceBudgetMicroseconds: 10000 + maxFilterLimit: 500 + maxSubsPerConnection: 20 + writePolicy: + plugin: /var/lib/strfry/copmlex-entrypoint.ts + lookbackSeconds: 0 + compression: + enabled: yes + slidingWindow: yes + logging: + dumpInAll: no + dumpInEvents: no + dumpInReqs: no + dbScanPerf: no + invalidEvents: yes + numThreads: + ingester: 3 + reqWorker: 3 + reqMonitor: 3 + negentropy: 2 + negentropy: + enabled: no + maxSyncEvents: 1000000 +strfry_events: + maxEventSize: 65536 + rejectEventsNewerThanSeconds: 900 + rejectEventsOlderThanSeconds: 94608000 + rejectEphemeralEventsOlderThanSeconds: 60 + ephemeralEventsLifetimeSeconds: 300 + maxNumTags: 2000 + maxTagValSize: 1024 \ No newline at end of file diff --git a/ansible/hosts.cfg b/ansible/hosts.cfg new file mode 100644 index 0000000..bf281d4 --- /dev/null +++ b/ansible/hosts.cfg @@ -0,0 +1,16 @@ +[observability] +garchomp.bitcoiner.social + +[mail] +garchomp.bitcoiner.social + +[certbot] +gabite.bitcoiner.social + +[strfry] +garchomp.bitcoiner.social +gabite.bitcoiner.social + +[all:vars] +ansible_user=jim +ansible_become_method=doas \ No newline at end of file diff --git a/ansible/playbooks/caddy.yml b/ansible/playbooks/caddy.yml new file mode 100644 index 0000000..a599186 --- /dev/null +++ b/ansible/playbooks/caddy.yml @@ -0,0 +1,5 @@ +--- +- hosts: all + roles: + - role: caddy + become: yes diff --git a/ansible/playbooks/group_tasks/certbot/main.yml b/ansible/playbooks/group_tasks/certbot/main.yml new file mode 100644 index 0000000..e215e9d --- /dev/null +++ b/ansible/playbooks/group_tasks/certbot/main.yml @@ -0,0 +1,40 @@ +--- +- hosts: certbot + + pre_tasks: + - import_tasks: tasks/dhparams.yml + tags: dhparams + + - name: Loop through the Certbot certificate list to configure nginx for each ACME domain + include_tasks: tasks/nginx_conf.yml + loop: "{{ certbot_certs }}" + loop_control: + loop_var: acme_domain + when: certbot_certs is defined + tags: nginx + + - name: Ensure html directory for certbot challenge + ansible.builtin.file: + path: /var/www/html + state: directory + mode: 0755 + become: yes + + - name: Remove default nginx page so it doesn't interfere with certbot + ansible.builtin.file: + path: /etc/nginx/conf.d/default.conf + state: absent + become: yes + + roles: + - role: geerlingguy.certbot + become: yes + tags: certbot + vars: + certbot_auto_renew: true + certbot_create_if_missing: true + + tasks: + - import_tasks: tasks/node_exporter.yml + tags: node_exporter + become: yes \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/certbot/tasks/dhparams.yml b/ansible/playbooks/group_tasks/certbot/tasks/dhparams.yml new file mode 100644 index 0000000..8c96ff6 --- /dev/null +++ b/ansible/playbooks/group_tasks/certbot/tasks/dhparams.yml @@ -0,0 +1,22 @@ +--- +- name: Register pre-generated dhparams + ansible.builtin.stat: + path: files/dhparams.pem + delegate_to: localhost + register: dhparams + tags: dhparams + +- name: Use pre-generated dhparams to reduce deployment time by several minutes. + ansible.builtin.copy: + src: dhparams.pem + dest: /etc/ssl/ + force: false + when: dhparams.stat.exists + become: yes + tags: dhparams + +- name: Generate Diffie-Hellman parameters with the default size (4096 bits) + community.crypto.openssl_dhparam: + path: /etc/ssl/dhparams.pem + become: yes + tags: dhparams diff --git a/ansible/playbooks/group_tasks/certbot/tasks/nginx_conf.yml b/ansible/playbooks/group_tasks/certbot/tasks/nginx_conf.yml new file mode 100644 index 0000000..b4e7d27 --- /dev/null +++ b/ansible/playbooks/group_tasks/certbot/tasks/nginx_conf.yml @@ -0,0 +1,70 @@ +--- +- name: Configure nginx for certbot challenge and TLSv1.2 for {{ acme_domain.domains[0] }} + ansible.builtin.import_role: + name: nginx_core.nginx_config + allow_duplicates: true + tags: nginx + become: yes + vars: + nginx_config_dhparam: /etc/ssl/dhparams.pem + nginx_config_http_template_enable: true + nginx_config_http_template: + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/acme_{{ acme_domain.domains[0] }}.conf" + backup: false + config: + core: + server_name: "{{ acme_domain.domains[0] }}" + ssl: + certificate: "/etc/letsencrypt/live/{{ acme_domain.domains[0] }}/fullchain.pem" + certificate_key: "/etc/letsencrypt/live/{{ acme_domain.domains[0] }}/privkey.pem" + trusted_certificate: "/etc/letsencrypt/live/{{ acme_domain.domains[0] }}/chain.pem" + ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + dhparam: "{{ nginx_config_dhparam }}" + ecdh_curve: X25519:secp521r1:secp384r1 + prefer_server_ciphers: true + protocols: + - TLSv1.2 + - TLSv1.3 + session_cache: + shared: + name: "{{ acme_domain.domains[0] }}" + size: 1M + session_tickets: false + session_timeout: 1d + ocsp: true + ocsp_cache: + name: cache + size: 64k + stapling: true + stapling_verify: true + ocsp_responder: http://r3.o.lencr.org + headers: + add_headers: + - name: Strict-Transport-Security + value: '"max-age=7776000"' + always: true + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/http_{{ acme_domain.domains[0] }}.conf" + backup: false + config: + servers: + - core: + server_name: "{{ acme_domain.domains[0] }}" + listen: + - address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}:80" + log: + access: + - off + locations: + - location: / + rewrite: # https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite + return: + url: "https://{{ acme_domain.domains[0] }}$request_uri" + code: 301 + - location: /.well-known/acme-challenge/ + core: + alias: /var/www/html/.well-known/acme-challenge/ + try_files: + files: $uri + code: =404 \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/certbot/tasks/node_exporter.yml b/ansible/playbooks/group_tasks/certbot/tasks/node_exporter.yml new file mode 100644 index 0000000..9febe20 --- /dev/null +++ b/ansible/playbooks/group_tasks/certbot/tasks/node_exporter.yml @@ -0,0 +1,26 @@ +--- +- name: Configure nginx + ansible.builtin.import_role: + name: nginx_core.nginx_config + tags: nginx + vars: + nginx_config_http_template_enable: true + nginx_config_http_template: + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/node-exp_{{ inventory_hostname }}.conf" + backup: false + config: + servers: + - core: + listen: + # Use IP address in Ansible facts. https://stackoverflow.com/q/39819378/9290 + - address: "{{ default_interface_ipv4_address|default(ansible_default_ipv4.address) }}" + ssl: true + port: 4430 + include: + - "/etc/nginx/acme_{{ inventory_hostname }}.conf" + locations: + - location: / + proxy: + pass: "http://127.0.0.1:8030" + http_version: 1.1 \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/lego.yml b/ansible/playbooks/group_tasks/lego.yml new file mode 100644 index 0000000..c00d72d --- /dev/null +++ b/ansible/playbooks/group_tasks/lego.yml @@ -0,0 +1,28 @@ +--- +- hosts: all, !proxmox + + pre_tasks: + - name: Gather facts about the localhost + ansible.builtin.setup: + delegate_to: localhost + +# - name: Print +# debug: +# var: ansible_facts +# - name: Assert that the local machine is squirtle +# ansible.builtin.assert: +# that: +# - ansible_facts['nodename'] == 'squirtle' +# msg: "This playbook should only be run from squirtle." + + roles: + - role: bleetube.lego + when: ansible_nodename == 'squirtle' + + tasks: + - name: Reload nginx + ansible.builtin.service: + name: nginx + state: reloaded + tags: nginx + become: yes \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/mail.yml b/ansible/playbooks/group_tasks/mail.yml new file mode 100644 index 0000000..aae7892 --- /dev/null +++ b/ansible/playbooks/group_tasks/mail.yml @@ -0,0 +1,25 @@ +--- +- hosts: mail + become: yes + + pre_tasks: + - name: Gather facts about the localhost + ansible.builtin.setup: + delegate_to: localhost + tags: lego + + roles: + - role: bleetube.lego + when: ansible_nodename == 'squirtle' + tags: lego + + - role: bleetube.disposable-mail + tags: mail + + tasks: + - name: Reload dovecot + ansible.builtin.service: + name: dovecot + state: reloaded + tags: lego + become: yes \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/nginx.yml b/ansible/playbooks/group_tasks/nginx.yml new file mode 100644 index 0000000..f3488c3 --- /dev/null +++ b/ansible/playbooks/group_tasks/nginx.yml @@ -0,0 +1,7 @@ +--- +- hosts: nginx, certbot + + roles: + - role: nginxinc.nginx_core.nginx + tags: nginx + become: yes diff --git a/ansible/playbooks/group_tasks/observability.yml b/ansible/playbooks/group_tasks/observability.yml new file mode 100644 index 0000000..30ba15b --- /dev/null +++ b/ansible/playbooks/group_tasks/observability.yml @@ -0,0 +1,24 @@ +--- +- hosts: observability + + roles: + - role: prometheus.prometheus.alertmanager + become: true + tags: alertmanager + - role: prometheus.prometheus.blackbox_exporter + become: true + tags: blackbox_exporter + - role: prometheus.prometheus.prometheus + become: true + tags: prometheus + - role: grafana.grafana.grafana + become: true + tags: grafana + - role: bleetube.ntfy + tags: ntfy + become: true + - role: bleetube.ntfy-alertmanager + tags: ntfy-alertmanager + - role: caddy + tags: caddy + become: true \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/postgresql.yml b/ansible/playbooks/group_tasks/postgresql.yml new file mode 100644 index 0000000..2577804 --- /dev/null +++ b/ansible/playbooks/group_tasks/postgresql.yml @@ -0,0 +1,22 @@ +--- +- hosts: postgresql, matrix + become: yes + + vars: + postgresql_apt_key_url: "https://www.postgresql.org/media/keys/ACCC4CF8.asc" + + # https://github.com/ANXS/postgresql/issues/523 + ansible_python_interpreter: "/usr/bin/python3" + + pre_tasks: + - name: Assert all database passwords have been configured. + ansible.builtin.assert: + that: + - item.pass is defined + - item.pass != '' + fail_msg: "FAILED: Database password for {{ item.name }} is not configured." + loop: "{{ postgresql_users }}" + no_log: yes + + roles: + - role: anxs.postgresql diff --git a/ansible/playbooks/group_tasks/strfry.yml b/ansible/playbooks/group_tasks/strfry.yml new file mode 100644 index 0000000..b4be0a0 --- /dev/null +++ b/ansible/playbooks/group_tasks/strfry.yml @@ -0,0 +1,5 @@ +--- +- hosts: strfry + roles: + - role: bleetube.strfry + tags: strfry \ No newline at end of file diff --git a/ansible/playbooks/group_tasks/wireguard.yml b/ansible/playbooks/group_tasks/wireguard.yml new file mode 100644 index 0000000..9c78ce8 --- /dev/null +++ b/ansible/playbooks/group_tasks/wireguard.yml @@ -0,0 +1,7 @@ +--- +- hosts: wireguard + + roles: + - role: bleetube.wireguard + become: true + tags: wireguard diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/README.md b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/README.md new file mode 100644 index 0000000..73fd045 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/README.md @@ -0,0 +1,9 @@ +# Manual steps + +Not here: + +* `/etc/wireguard/lanturn.conf` +* `/var/lib/strfry/` stuff: + * pubkeys.db + * pubkeys.banned.ts + * pubkeys.premium.json diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/bitcoiner.social.conf b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/bitcoiner.social.conf new file mode 100644 index 0000000..8f28dae --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/bitcoiner.social.conf @@ -0,0 +1,94 @@ +# +# Ansible managed +# + +upstream strfry { + server 127.0.0.1:8000; +} + +map $http_accept $is_accept_nostr { + default "0"; + "application/nostr+json" "1"; +} + +# Combine the Upgrade and Accept headers +map "$http_upgrade:$is_accept_nostr" $is_socket_or_nostr { + default "0"; + "websocket:0" "1"; + "websocket:1" "1"; + ":1" "1"; +} +map "$http_upgrade:$is_accept_nostr" $is_nostr_and_not_socket { + default "0"; + "websocket:0" "0"; + "websocket:1" "0"; + ":1" "1"; +} + +server { + client_max_body_size 0; + include /etc/nginx/acme_bitcoiner.social.conf; + listen 443 ssl; + listen [::]:443 ssl; + + access_log off; + + location / { + +# Intercept NIP-11 + if ($is_nostr_and_not_socket = 1) { + return 200 '{"contact":"nostr@bitcoiner.social","description":"A fast, reliable, and up-to-date nostr relay with monitored server availability and nightly off-site backups.","name":"bitcoiner.social","pubkey":"ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b","software":"git+https://github.com/hoytech/strfry.git","supported_nips":[1,2,4,9,11,12,16,20,22,28,33,40],"version":"0.9.6","icon":"https://bitcoiner.social/favicon.ico"}'; + } + +# Web Browsers +# if ($is_socket_or_nostr = 0) { +# return 301 https://www.offchain.pub; +# } + + + proxy_connect_timeout 3m; + proxy_http_version 1.1; + proxy_pass http://strfry; + proxy_read_timeout 3m; + proxy_send_timeout 3m; + proxy_set_header Host $http_host; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + + + } + location /static { + alias /var/www/static; + } + location /.well-known/nostr.json { + alias /var/www/.well-known/nostr.json; + + add_header Access-Control-Allow-Origin *; + + + } + location /favicon.ico { + alias /var/www/static/favicon96.png; + + + } + location = /.well-known/matrix/server { + return 200 '{"m.server":"matrix.bitcoiner.social:443"}' + ; + + + } + location ~ ^(/_matrix|/_synapse/client) { + return 301 https://matrix.bitcoiner.social$request_uri; + + + } + location /.well-known/lnurlp/ { + proxy_pass http://10.19.21.42:8037; + proxy_set_header Host $http_host; + + add_header Access-Control-Allow-Origin *; + } +} diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/nostr.bitcoiner.social.conf b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/nostr.bitcoiner.social.conf new file mode 100644 index 0000000..759dff4 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/nostr.bitcoiner.social.conf @@ -0,0 +1,33 @@ +server { + include /etc/nginx/acme_nostr.bitcoiner.social.conf; + listen 443 ssl; + listen [::]:443 ssl; + + access_log off; + + location / { + + # Intercept NIP-11 + if ($is_nostr_and_not_socket = 1) { + return 200 '{"contact":"nostr@bitcoiner.social","description":"A fast, reliable, and up-to-date nostr relay with monitored server availability and nightly off-site backups.","name":"bitcoiner.social","pubkey":"ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b","software":"git+https://github.com/hoytech/strfry.git","supported_nips":[1,2,4,9,11,12,16,20,22,28,33,40],"version":"0.9.6","icon":"https://bitcoiner.social/favicon.ico"}'; + } + + # Web Browsers + #if ($is_socket_or_nostr = 0) { + # return 301 https://www.offchain.pub; + #} + + + proxy_connect_timeout 3m; + proxy_http_version 1.1; + proxy_pass http://strfry; + proxy_read_timeout 3m; + proxy_send_timeout 3m; + proxy_set_header Host $http_host; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } +} diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/tor_bitcoiner.social.conf b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/tor_bitcoiner.social.conf new file mode 100644 index 0000000..c7f6c8c --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nginx/tor_bitcoiner.social.conf @@ -0,0 +1,30 @@ +server { + listen 127.0.0.1:9080; + + access_log off; + + location / { + + # Intercept NIP-11 + if ($is_nostr_and_not_socket = 1) { + return 200 '{"contact":"nostr@bitcoiner.social","description":"A fast, reliable, and up-to-date nostr relay with monitored server availability and nightly off-site backups.","name":"bitcoiner.social","pubkey":"ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b","software":"git+https://github.com/hoytech/strfry.git","supported_nips":[1,2,4,9,11,12,16,20,22,28,33,40],"version":"0.9.6","icon":"https://bitcoiner.social/favicon.ico"}'; + } + + proxy_connect_timeout 3m; + proxy_http_version 1.1; + proxy_pass http://strfry; + proxy_read_timeout 3m; + proxy_send_timeout 3m; + proxy_set_header Host $http_host; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + + } + location /favicon.ico { + alias /var/www/static/favicon96.png; + + + } +} diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip11.json b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip11.json new file mode 100644 index 0000000..1f6971a --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip11.json @@ -0,0 +1 @@ +{"contact":"nostr@bitcoiner.social","description":"A fast, reliable, and up-to-date nostr relay with monitored server availability and nightly off-site backups.","name":"bitcoiner.social","pubkey": "ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b","software":"git+https://github.com/hoytech/strfry.git","supported_nips":[1,2,4,9,11,12,16,20,22,28,33,40],"version":"0.9.6","icon":"https://bitcoiner.social/favicon.ico"} diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip5.json b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip5.json new file mode 100644 index 0000000..18ebad7 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/nip5.json @@ -0,0 +1 @@ +{"names": {"_": "ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b", "admin": "ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b", "blee": "69a0a0910b49a1dbfbc4e4f10df22b5806af5403a228267638f2e908c968228d", "heady_wook": "dee97518077808091745015441953ad814c22d4eaeaba6996385a3d7dc3d191e", "ToxiKat27": "12cfc2ec5a39a39d02f921f77e701dbc175b6287f22ddf0247af39706967f1d9", "ruti": "6d2a43408ee74c2e35fd7f4eb3288b73cfafd88cd2d059e6c05058e676fc5290", "harmony": "3c0a6d434e28f68a4b31d38ece15b33c1518aa58299b7d315426d716c5493ed4", "minnaar": "8f9efe5e5b5b13fa967568f037811e1d8fa8ece3101a8c65ad0a5d75665fe6da", "arra": "e8c7df1dfef1d97c1acb27e77ef3262faf349bfb333642b778fe15c48bebae52", "natoshi": "bc815d16028c8b012103018596386b77cbd6ef27f10f1992ada4986104df0cab", "testing": "83768054ef906ca493dcf703dd93b73fa71054ec80f83e71e2eb68eb5139e1d6", "w_s_bitcoin": "af2f682f512899852c6654ff065aa405a41d15e2b1cb6c9173123605744f06e6", "flippo": "76482d5e45bc5a595a1501ae4f603ab52191948ebd7899cee9e4ab9e4ecb9987", "bitcoinbabybee": "f6499fae7651521774864664c44dd97d10489d69b222ff2596614cbb15c39278", "OutlanderBC": "bc10bd1557c52725ab7a45e193b8db08fcd889c7b3c89294bbb8df1ad693c8ad", "oz": "f91f2529318cb52308613485d36328214946679c96458c9c8f966fec347ea758", "ZapCats": "c13b48ff647b105a5c41eb7cb48694f93ce903e873fab83c2b96efa7bfa1eb13", "GiveawayGames": "f717d66780a2393589d4e84bf3010d860790234549c2f5260706226905d8922a", "rakan": "b377757fa3efd9d4f56170bd08508872b13680a000be9b19f3c0f6fea3d861bc", "kbbq": "c03be92f51cc5ca98b26bb279a45d670d0ee3c8e0779fa791b22d33f88ed4c59"}, "relays": {"69a0a0910b49a1dbfbc4e4f10df22b5806af5403a228267638f2e908c968228d": ["wss://bitcoiner.social"], "ece3317bf8163930b5dafae50596b740b0608433b78568886a9a712a91a5d59b": ["wss://bitcoiner.social"]}} diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/1984.py b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/1984.py new file mode 100644 index 0000000..51f851a --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/1984.py @@ -0,0 +1,181 @@ +import subprocess +import json +import csv +import logging +import time +from datetime import datetime, timedelta + +logging.basicConfig( + format='%(asctime)s %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + level=logging.INFO +) +console = logging.StreamHandler() +console.setLevel(logging.INFO) +formatter = logging.Formatter('%(levelname)s: %(message)s') +console.setFormatter(formatter) +logging.getLogger('').addHandler(console) +logger = logging.getLogger(__name__) + + +def get_reports(days_since: int = 1): + unhandled_reports = 0 + try: + nostr_filter = json.dumps({ + "kinds":[1984], + "since": get_timestamp(days_since) + }) + logging.info(nostr_filter) + result = subprocess.run( + ["strfry", "scan", nostr_filter], + capture_output=True, text=True, check=True + ) + strfry_reports = result.stdout.splitlines() + logging.info(f"{len(strfry_reports)} reports have been received.") + + report_data = {} + for strfry_report in strfry_reports: + report = json.loads(strfry_report) + + pubkey = None + event = None + try: + for tag in report.get('tags', []): + if tag[0] == 'p': + pubkey = tag[1] + if tag[0] == 'e': + event = (tag[1], tag[2]) + + if pubkey and event: + if pubkey not in report_data: + report_data[pubkey] = {'count': 0, 'events': list()} + report_data[pubkey]['count'] += 1 + report_data[pubkey]['events'].append(event) + elif pubkey: + if pubkey not in report_data: + report_data[pubkey] = {'count': 0, 'events': list()} + report_data[pubkey]['count'] += 1 + else: + # TODO: Handle NIP-69, maybe + logging.warning(f"Un-handled report type: {report}") + unhandled_reports += 1 + except: + logging.error(f"Invalid report: {report}") + unhandled_reports += 1 + + logging.warning(f"Unhandled reports: {unhandled_reports}") + return report_data + + except subprocess.CalledProcessError as e: + logging.error("Error Output:", e.stderr) + +def display_user_posts(user_id): + """ Display posts of a given user """ + + try: + nostr_filter = json.dumps({"authors":[user_id],"limit":20}) + result = subprocess.run( + ["strfry", "scan", nostr_filter], + capture_output=True, text=True, check=True + ) + try: + posts = json.loads(result.stdout) + for post in posts: + if post.get("kind") == 1: + print(post.get("content", "")[:240]) + except: + print(result.stdout) + except subprocess.CalledProcessError as e: + #print("Error occurred:", e) + #print("Standard Output:", e.stdout) + print("Error Output:", e.stderr) + +def delete_user_content(user_id): + """ Delete content of a given user """ + + try: + nostr_filter = json.dumps({"authors":[user_id]}) + subprocess.run( + ["strfry", "delete", "--filter", nostr_filter], + check=True + ) + except subprocess.CalledProcessError as e: + print("Error deleting content:", e) + +def check_user(user_id: str, report_count): + display_user_posts(user_id) + print(f"Displaying posts for user: {user_id}") + + # ask to ban the user + answer = input(f"Do you want to ban this user that was reported {report_count} times? (y/N): ") + if answer.lower() == 'y': + print(f"Banning user: {user_id}") + ban_user(user_id) + else: + print(f"Skipping ban for user: {user_id}") + return + + answer = input("Do you want to delete this user's content? (y/N): ") + if answer.lower() == 'y': + print(f"Deleting content for user: {user_id}") + delete_user_content(user_id) + else: + print(f"Skipping deletion for user: {user_id}") + +def ban_user(user_id): + """Ban the user by writing the user_id to a file""" + + with open("banned_users.txt", "a") as f: + f.write(f"{user_id}\n") + +def is_user_banned(user_id): + """Check if the user is banned""" + + with open("banned_users.txt", "r") as f: + banned_users = f.read().splitlines() + return user_id in banned_users + +def read_csv_to_list(file_path) -> list: + """ + Reads a CSV file and returns a list of tuples. + Each tuple contains the report count and reported public key from each row. + + :param file_path: Path to the CSV file. + :return: List of tuples (report_count, reported_pubkey). + """ + + data = [] + with open(file_path, 'r') as file: + reader = csv.reader(file) + for row in reader: + # Convert the first element (count) to an integer + report_count = int(row[0]) + reported_pubkey = row[1] + data.append((report_count, reported_pubkey)) + logging.info(f"{len(data)} npubs have been reported during this period.") + return data + +def get_timestamp(days_ago: int) -> int: + """ + Returns a Unix timestamp for a given number of days ago. + + :param days_ago: Number of days ago. + :return: Unix timestamp. + """ + time_since = datetime.now() - timedelta(days=days_ago) + return int(time.mktime(time_since.timetuple())) + + +def main(): + unsorted_reports = get_reports(7) + sorted_reports = sorted(unsorted_reports.items(), key=lambda x: x[1]['count'], reverse=True) + reports = dict(sorted_reports) + for reported_npub in reports: + report = reports[reported_npub] + if report['count'] > 5: + for event in report['events']: + print(event) + check_user(reported_npub, report['count']) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/compact-strfry-database.sh b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/compact-strfry-database.sh new file mode 100755 index 0000000..15bde8c --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/compact-strfry-database.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -x +set -e + +# compact +doas -u strfry strfry compact strfry-db/compact.mdb +systemctl stop strfry +mv -v strfry-db/compact.mdb strfry-db/data.mdb +systemctl start strfry \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/prune.ts b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/prune.ts new file mode 100644 index 0000000..e971d6b --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/prune.ts @@ -0,0 +1,68 @@ +import { readLines } from 'https://deno.land/std@0.201.0/io/mod.ts'; + +import { DB } from "https://deno.land/x/sqlite@v3.8/mod.ts"; + +const db = new DB("pubkeys.db"); +const subscriberResults = db.query("SELECT pubkey FROM subscribers ORDER BY pubkey DESC") as string[][]; +const foafResults = db.query("SELECT DISTINCT foaf_pubkey FROM foaf ORDER BY foaf_pubkey DESC") as string[][]; +const subscriberSet = new Set(subscriberResults.map(subscriber => subscriber[0])); +const foafSet = new Set(foafResults.map(foaf => foaf[0])); + +const trustEventAgeLimit = 90; +const foafEventAgeLimit = 30; +const untrustEventAgeLimit = 7; + +const importantKindAgeLimits: Record = { + 0: 365, // NIP-01: profiles + 3: 365, // NIP-01: contacts + 9735: 180, // NIP-57: zap receipts + 24133: 365, // NIP-46: nostr connect + 13194: 365, // NIP-47: nostr wallet connect + 10002: 730, // NIP-65 +}; + +const exportEventsStdin = async (): Promise => { + if (Deno.isatty(Deno.stdin.rid)) { + Deno.exit(1); + } + for await (const line of readLines(Deno.stdin)) { + if (line.length === 0) { + return; + } + exportLine(line); + } +}; + +const exportLine = (line: string): void => { + const eventJson = JSON.parse(line); + const created_at = new Date(eventJson.created_at * 1000); // convert seconds to ms + + if (subscriberSet.has(eventJson.pubkey)) { + + // Tier 1: subscribers + const kindAgeLimit = (importantKindAgeLimits[eventJson.kind] || trustEventAgeLimit) * 24 * 60 * 60 * 1000; // convert days to ms + const ageLimitDate = new Date(Date.now() - kindAgeLimit); + if (created_at > ageLimitDate) { + console.log(line); // keep events from the last year + } + } else if (foafSet.has(eventJson.pubkey)) { + + // Tier 2: foaf + const kindAgeLimit = (importantKindAgeLimits[eventJson.kind] || foafEventAgeLimit) * 24 * 60 * 60 * 1000; // convert days to ms + const ageLimitDate = new Date(Date.now() - kindAgeLimit); + if (created_at > ageLimitDate) { + console.log(line); // keep events from the last year + } + } else { + + // Tier 3: untrust + const kindAgeLimit = (importantKindAgeLimits[eventJson.kind] || untrustEventAgeLimit) * 24 * 60 * 60 * 1000; // convert days to ms + const ageLimitDate = new Date(Date.now() - kindAgeLimit); + if (created_at > ageLimitDate) { + console.log(line); // keep recent events + } + + } +}; + +await exportEventsStdin(); \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/pruning.sh b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/pruning.sh new file mode 100755 index 0000000..ef729ee --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/scripts/pruning.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -x +set -e + +echo Beginning pruning task. + +# prune +strfry export | deno run --allow-read=. --allow-write=pubkeys.db prune.ts > /tmp/pruning.jsonl +mv -v strfry-db/data.mdb strfry-db/archive.mdb +cat /tmp/pruning.jsonl | strfry import --no-verify + +# compact +strfry compact strfry-db/compact.mdb +mv -v strfry-db/compact.mdb strfry-db/data.mdb + +echo Pruning task completed. diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.service b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.service new file mode 100644 index 0000000..44bef13 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.service @@ -0,0 +1,9 @@ +[Unit] +Description=Compact and defragment strfry database + +[Service] +WorkingDirectory=/var/lib/strfry +ExecStart=/var/lib/strfry/compact-strfry-database.sh + +[Install] +WantedBy=multi-user.target diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.timer b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.timer new file mode 100644 index 0000000..46061f0 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/compact-strfry-database.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Compact and defragment strfry database + +[Timer] +# Every Sunday +OnCalendar=Sun *-*-* 1:00:00 + +[Install] +WantedBy=default.target diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.service b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.service new file mode 100644 index 0000000..32fbd8e --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.service @@ -0,0 +1,11 @@ +Unit] +Description=Prune strfry database + +[Service] +WorkingDirectory=/var/lib/strfry +ExecStartPre=systemctl stop strfry +ExecStart=doas -u strfry /var/lib/strfry/pruning.sh +ExecStartPost=systemctl start strfry + +[Install] +WantedBy=multi-user.target diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.timer b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.timer new file mode 100644 index 0000000..c78ce23 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/files/systemd/prune-strfry-database.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Prune strfry database + +[Timer] +# Run every month on the second Sunday +OnCalendar=Sun *-*-8..14 1:00:00 + +[Install] +WantedBy=default.target diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/main.yml b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/main.yml new file mode 100644 index 0000000..3118ec0 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/main.yml @@ -0,0 +1,85 @@ +--- +- hosts: gabite.bitcoiner.social + become: yes + + handlers: + - name: reload nginx + ansible.builtin.service: + name: nginx + state: reloaded + - name: reload systemd + ansible.builtin.systemd: + daemon_reload: yes + tags: systemd + + tasks: + - name: Ensure well-known path + ansible.builtin.file: + path: /var/www/.well-known + state: directory + + - name: Restore nip-05 from backup + ansible.builtin.copy: + src: files/nip5.json + dest: /var/www/.well-known/nostr.json + force: no + + - name: Copy manually defined nip-11 to well-known path + ansible.builtin.copy: + src: files/nip11.json + dest: /var/www/nip11.json + force: no + + - name: Configure complex nginx proxy settings + ansible.builtin.copy: + src: "nginx/{{ item }}" + dest: "/etc/nginx/conf.d/{{ item }}" + loop: + - bitcoiner.social.conf + - nostr.bitcoiner.social.conf + - tor_bitcoiner.social.conf + notify: reload nginx + tags: nginx + + - include_tasks: nginx_conf.yml + tags: nginx + + - name: Set vm.swappiness in /etc/sysctl.conf + ansible.builtin.lineinfile: + path: /etc/sysctl.conf + regexp: '^vm.swappiness' + line: 'vm.swappiness=10' + state: present + register: swappiness + tags: swap + + - name: Apply sysctl changes + ansible.builtin.command: + cmd: sysctl -p + when: swappiness.changed + tags: swap + + - name: Configure custom strfry scripts + ansible.builtin.copy: + src: "files/scripts/{{ item }}" + dest: "/var/lib/strfry/{{ item }}" + owner: strfry + group: strfry + mode: '0755' + loop: + - compact-strfry-database.sh + - pruning.sh + - prune.ts + tags: copy + + - name: Copy systemd service files + ansible.builtin.copy: + src: "files/systemd/{{ item }}" + dest: "/etc/systemd/system/{{ item }}" + loop: + - prune-strfry-database.service + - prune-strfry-database.timer + - compact-strfry-database.service + - compact-strfry-database.timer + tags: systemd + notify: reload systemd \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/gabite.bitcoiner.social/nginx_conf.yml b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/nginx_conf.yml new file mode 100644 index 0000000..7300c67 --- /dev/null +++ b/ansible/playbooks/host_tasks/gabite.bitcoiner.social/nginx_conf.yml @@ -0,0 +1,90 @@ +--- +- name: Configure nginx + ansible.builtin.import_role: + name: nginx_core.nginx_config + vars: + # overriding any numeric values in the main nginx config requires replacing the entire dictionary + # See: https://github.com/nginxinc/ansible-role-nginx-config/issues/352 + nginx_config_main_template_enable: true + nginx_config_main_template: + template_file: nginx.conf.j2 + deployment_location: /etc/nginx/nginx.conf + backup: false + config: # https://nginx.org/en/docs/ngx_core_module.html + main: + user: + username: nginx + group: nginx + worker_processes: auto + error_log: + file: /var/log/nginx/error.log + level: notice + #pid: /var/run/nginx.pid + + # worker_rlimit_nofile changes the limit on the maximum number of open files (RLIMIT_NOFILE) for worker processes. + # Used to increase the limit without restarting the main process. + # The recommended value seems to be worker_connections * 2 + worker_rlimit_nofile: 12288 + + events: + worker_connections: 4096 + + http: # https://nginx.org/en/docs/http/ngx_http_core_module.html + default_type: application/octet-stream + sendfile: true + server_tokens: false + tcp_nodelay: true + tcp_nopush: true + include: + - /etc/nginx/mime.types + - /etc/nginx/http.conf + - /etc/nginx/conf.d/*.conf + + nginx_config_http_template_enable: true + nginx_config_http_template: + - template_file: http/default.conf.j2 + deployment_location: /etc/nginx/http.conf + backup: false + config: + core: + default_type: application/octet-stream + sendfile: true + server_tokens: false + tcp_nodelay: true + tcp_nopush: true + resolver: # required for oscp stapling + address: + - '1.1.1.1' + - '8.8.8.8' + resolver_timeout: 10s + log: + format: + - name: main + format: | + '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" "$realip_remote_addr"' + gzip: # https://nginx.org/en/docs/http/ngx_http_gzip_module.html + enable: true + comp_level: 9 + min_length: 100 + proxied: any + types: + - application/json + - text/plain + - text/css + vary: true + + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/mappings.conf" + backup: false + config: + map: + mappings: # https://nginx.org/en/docs/http/websocket.html + - string: $http_upgrade + variable: $connection_upgrade + content: + - value: default + new_value: upgrade + - value: "''" + new_value: close diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/files/hablanews.service b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/files/hablanews.service new file mode 100644 index 0000000..fa1a480 --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/files/hablanews.service @@ -0,0 +1,13 @@ +[Unit] +Description=Habla News + +[Service] +User=news +Group=news +WorkingDirectory=/var/www/habla/news +ExecStart=pnpm start +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/hablanews.yml b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/hablanews.yml new file mode 100644 index 0000000..48809da --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/hablanews.yml @@ -0,0 +1,79 @@ +--- +- hosts: garchomp.bitcoiner.social + + handlers: + - name: restart hablanews + ansible.builtin.service: + name: hablanews + state: restarted + become: yes + + roles: + - role: bleetube.nodejs + become: yes + tags: nodejs + tasks: + #- name: Create a hablanews group + # ansible.builtin.group: + # name: hablanews + # state: present + # become: yes + # tags: group + + - name: Create a news user + ansible.builtin.user: + shell: /bin/bash + createhome: no + home: /var/www/habla + name: news + group: news + append: yes + become: yes + tags: user + + - name: Create directory owned by news + ansible.builtin.file: + path: /var/www/habla/news + state: directory + owner: news + group: news + mode: '0755' + become: yes + tags: directory + tags: git + + - name: Clone git repository + ansible.builtin.git: + repo: https://github.com/bleetube/habla.news + dest: /var/www/habla/news + version: bitcoiner.social + force: true + become: yes + become_user: news + register: git_repository + tags: git + + - name: Build habla.news + ansible.builtin.command: + cmd: "{{ item }}" + chdir: /var/www/habla/news + become: yes + become_user: news + tags: build + notify: restart hablanews + loop: + - pnpm install + - pnpm build + + - name: Install service unit + ansible.builtin.copy: + src: hablanews.service + dest: /etc/systemd/system/hablanews.service + become: yes + tags: systemd + + - name: Reload systemd + ansible.builtin.systemd: + daemon_reload: yes + become: yes + tags: systemd \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/nginx.yml b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/nginx.yml new file mode 100644 index 0000000..a3cb832 --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/nginx.yml @@ -0,0 +1,146 @@ +--- +- name: strfry | Configure nginx + ansible.builtin.import_role: + name: nginx_core.nginx_config + vars: + # afaict, overriding any numeric values in the main nginx config requires replacing the entire dictionary. + # See: https://github.com/nginxinc/ansible-role-nginx-config/issues/352 + # The only difference between this and the nginx config used in playbooks/nginx/main.yml is the worker_rlimit_nofile value and worker_connections. + nginx_config_main_template_enable: true + nginx_config_main_template: + template_file: nginx.conf.j2 + deployment_location: /etc/nginx/nginx.conf + backup: false + config: # https://nginx.org/en/docs/ngx_core_module.html + main: + user: + username: nginx + group: nginx + worker_processes: auto + error_log: + file: /var/log/nginx/error.log + level: notice + #pid: /var/run/nginx.pid + + # worker_rlimit_nofile changes the limit on the maximum number of open files (RLIMIT_NOFILE) for worker processes. + # Used to increase the limit without restarting the main process. + # The recomended value seems to be worker_connections * 2 + worker_rlimit_nofile: 12288 + + events: + worker_connections: 4096 + + # include: # String or a list of strings + # - /etc/nginx/modules.conf + http: # https://nginx.org/en/docs/http/ngx_http_core_module.html + default_type: application/octet-stream + sendfile: true + server_tokens: false + tcp_nodelay: true + tcp_nopush: true + include: + - /etc/nginx/mime.types + - /etc/nginx/http.conf # These are shared http level configs that nginx_conf refuses to directly configure. + - /etc/nginx/conf.d/*.conf + + nginx_config_http_template_enable: true + nginx_config_http_template: + - template_file: http/default.conf.j2 + deployment_location: /etc/nginx/http.conf + backup: false + config: + core: + default_type: application/octet-stream + sendfile: true + server_tokens: false + tcp_nodelay: true + tcp_nopush: true + resolver: # required for oscp stapling + address: + - '1.1.1.1' + - '8.8.8.8' + resolver_timeout: 10s + log: + format: + - name: main + format: | + '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" "$realip_remote_addr"' + # - name: debugposts + # format: | + # '$remote_addr - $remote_user [$time_local] "$request" ' + # '$status $body_bytes_sent "$http_referer" ' + # '"$http_user_agent" "$http_x_forwarded_for" "$realip_remote_addr"' + # '"$request_data"' + gzip: # https://nginx.org/en/docs/http/ngx_http_gzip_module.html + enable: true + comp_level: 3 + disable: "msie6" + min_length: 1100 + proxied: any + types: + - text/plain + - text/css + - application/x-javascript + - text/xml + - application/xml + vary: true + + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/mappings.conf" + backup: false + config: + map: + mappings: # https://nginx.org/en/docs/http/websocket.html + - string: $http_upgrade + variable: $connection_upgrade + content: + - value: default + new_value: upgrade + - value: "''" + new_value: close + + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/snort_{{ nginx_snort_domain|default(inventory_hostname) }}.conf" + backup: false + config: + servers: + - core: + listen: + - address: "{{ default_interface_ipv4_address|default(ansible_default_ipv4.address) }}:{{ nginx_snort_port|default(4451) }} ssl" + include: + - "/etc/nginx/acme_{{ nginx_snort_domain|default(inventory_hostname) }}.conf" + index: index.html + #root: "{{ snort_install_path|default('/var/www/snort') }}" + log: + access: + - off + http2: + enabled: yes + locations: + - location: / + core: + try_files: + files: "{{ snort_install_path|default('/var/www/snort') }}/packages/app/public/ {{ snort_install_path|default('/var/www/snort') }}/packages/app/build/ @proxy" + #files: $uri $uri/ /index.html + - location: '@proxy' + proxy: + pass: http://localhost:8080 # 127.0.0.1 does not work. + http_version: '1.1' + #set_header: + # - field: Host + # value: $http_host + - core: + server_name: "{{ nginx_snort_domain|default(inventory_hostname) }}" + listen: + - address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}:80" + log: + access: + - off + locations: + - location: / + rewrite: + return: + url: https://$server_name$request_uri + code: 301 \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry.yml b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry.yml new file mode 100644 index 0000000..714b8fd --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry.yml @@ -0,0 +1,8 @@ +--- +- hosts: garchomp.bitcoiner.social + become: yes + tasks: + - import_tasks: strfry/nginx_conf.yml + tags: nginx + # - import_tasks: strfry/logrotate.yml + # tags: logrotate \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/logrotate.yml b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/logrotate.yml new file mode 100644 index 0000000..defdc76 --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/logrotate.yml @@ -0,0 +1,18 @@ +--- +- name: Install logrotate + ansible.builtin.package: + name: logrotate + state: present + +- name: Rotate strfry plugin logs + ansible.builtin.blockinfile: + path: /etc/logrotate.d/strfry + state: present + create: true + block: | + /var/lib/strfry/plugin/plugin.log { + daily + rotate 3 + create + truncate + } diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/nginx_conf.yml b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/nginx_conf.yml new file mode 100644 index 0000000..f1af244 --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/nginx_conf.yml @@ -0,0 +1,228 @@ +--- +- name: Install redirect template for Snort + ansible.builtin.template: + src: templates/snort_redirect.conf.j2 + dest: /etc/nginx/snort_redirect.conf + tags: nginx + +- name: strfry | Configure nginx + ansible.builtin.import_role: + name: nginx_core.nginx_config + become: true + vars: + nginx_config_http_template_enable: true + nginx_config_http_template: + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/strfry_{{ nginx_strfry_domain }}.conf" + backup: false + config: + upstreams: + - name: strfry + servers: + - address: "127.0.0.1:{{ strfry_relay.port|default(7777) }}" + #- address: unix:/var/lib/strfry/strfry.sock + servers: + - core: + listen: + - address: "{{ default_interface_ipv4_address|default(ansible_default_ipv4.address) }}:{{ nginx_strfry_https_port|default(443) }} ssl" + # - address: "[2607:f130:0:105:216:3cff:fefb:92c2]:443 ssl" + include: + - "/etc/nginx/acme_{{ nginx_strfry_domain }}.conf" + #- /etc/nginx/snort_redirect.conf # breaks amethyst relay profile + client_max_body_size: 0 # Stream request body to backend + log: + access: + - off + locations: + - location: / + proxy: + pass: http://strfry + http_version: '1.1' + set_header: + - field: Host + value: $http_host + - field: Connection + value: $connection_upgrade + - field: Upgrade + value: $http_upgrade + - field: X-Forwarded-For + value: $proxy_add_x_forwarded_for + connect_timeout: 3m + send_timeout: 3m + read_timeout: 3m + - location: /static + core: + alias: /var/www/static + - location: /.well-known/nostr.json + core: + alias: /var/www/static/nostr.json + headers: + add_headers: + - name: Access-Control-Allow-Origin + value: '*' + - location: /favicon.ico + core: + alias: /var/www/static/favicon96.png + # https://matrix-org.github.io/synapse/latest/delegate.html + - location: '= /.well-known/matrix/server' + rewrite: # https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite + return: + code: 200 + text: > + '{"m.server":"matrix.bitcoiner.social:443"}' + - location: '~ ^(/_matrix|/_synapse/client)' + rewrite: + return: + url: "https://matrix.bitcoiner.social$request_uri" + code: 301 + - location: = /blee + rewrite: + return: + url: https://snort.bitcoiner.social/p/npub1dxs2pygtfxsah77yuncsmu3ttqr274qr5g5zva3c7t5s3jtgy2xszsn4st + code: 301 + + # nostr.bitcoiner.social + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/nostr.bitcoiner.social.conf" + backup: false + config: + servers: + - core: + listen: + - address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}:443 ssl" + include: + - "/etc/nginx/acme_nostr.bitcoiner.social.conf" + log: + access: + - off + locations: + - location: / + proxy: + pass: http://strfry + http_version: '1.1' + set_header: + - field: Host + value: $http_host + - field: Connection + value: $connection_upgrade + - field: Upgrade + value: $http_upgrade + - field: X-Forwarded-For + value: $proxy_add_x_forwarded_for + connect_timeout: 3m + send_timeout: 3m + read_timeout: 3m + # headers: + # add_headers: + # - name: Access-Control-Allow-Origin + # value: '*' + # limit_req: # https://www.nginx.com/blog/rate-limiting-nginx/ + # limit_reqs: # see files/limits.conf + # - zone: nostr + # burst: 5 + # delay: false + + # bitcoinr6de5lkvx4tpwdmzrdfdpla5sya2afwpcabjup2xpi5dulbad.onion + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/tor_{{ nginx_strfry_domain }}.conf" + backup: false + config: + servers: + - core: + listen: + - address: "127.0.0.1:9080" + log: + access: + - off + locations: + - location: / + proxy: + pass: http://strfry + http_version: '1.1' + set_header: + - field: Host + value: $http_host + - field: Connection + value: $connection_upgrade + - field: Upgrade + value: $http_upgrade + - field: X-Forwarded-For + value: $proxy_add_x_forwarded_for + connect_timeout: 3m + send_timeout: 3m + read_timeout: 3m + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/http_{{ nginx_strfry_domain }}.conf" + backup: false + config: + servers: + - core: + server_name: "{{ nginx_strfry_domain }}" + listen: + - address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}:80" + log: + access: + - off + locations: + - location: / + rewrite: + return: + url: https://$server_name$request_uri + code: 301 + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/cast.bitcoiner.social.conf" + backup: false + config: + servers: + - core: + listen: + - address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}:443 ssl" + include: + - "/etc/nginx/acme_cast.bitcoiner.social.conf" + log: + access: + - off + locations: + - location: / + rewrite: + return: + url: "https://modusb.com$request_uri" + code: 301 + - location: = /@lacosanostr/feed.xml + rewrite: + return: + url: https://modusb.com/LCN.rss + code: 301 + - template_file: http/default.conf.j2 + deployment_location: "/etc/nginx/conf.d/news.bitcoiner.social.conf" + backup: false + config: + servers: + - core: + listen: + - address: "{{ default_interface_ipv4_address|default(ansible_default_ipv4.address) }}:443 ssl" + include: + - "/etc/nginx/acme_news.bitcoiner.social.conf" + log: + access: + - off + locations: + - location: / + proxy: + pass: http://127.0.0.1:3000 + http_version: '1.1' + set_header: + - field: Host + value: $http_host + - core: + listen: + - address: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}:80" + log: + access: + - off + locations: + - location: / + rewrite: + return: + url: https://$server_name$request_uri + code: 301 \ No newline at end of file diff --git a/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/templates/snort_redirect.conf.j2 b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/templates/snort_redirect.conf.j2 new file mode 100644 index 0000000..9d92f38 --- /dev/null +++ b/ansible/playbooks/host_tasks/garchomp.bitcoiner.social/strfry/templates/snort_redirect.conf.j2 @@ -0,0 +1,14 @@ + location = / { + if ($connection_upgrade = "close") { + return 302 https://{{ nginx_snort_domain }}/global; + } + proxy_connect_timeout 3m; + proxy_http_version 1.1; + proxy_pass http://strfry; + proxy_read_timeout 3m; + proxy_send_timeout 3m; + proxy_set_header Host $http_host; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } diff --git a/ansible/playbooks/linux.yml b/ansible/playbooks/linux.yml new file mode 100644 index 0000000..c8a4786 --- /dev/null +++ b/ansible/playbooks/linux.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + roles: + - role: bleetube.linux + tags: linux + become: yes + - role: bleetube.dotfiles + tags: dotfiles \ No newline at end of file diff --git a/ansible/playbooks/main.yml b/ansible/playbooks/main.yml new file mode 100644 index 0000000..fa86c41 --- /dev/null +++ b/ansible/playbooks/main.yml @@ -0,0 +1,16 @@ +--- # Run playbooks in a specific order +- import_playbook: linux.yml +- import_playbook: nginx/main.yml +- import_playbook: group_tasks/nginx.yml +- import_playbook: group_tasks/certbot/main.yml +#- import_playbook: group_tasks/lego.yml # squirtle only +- import_playbook: node_exporter.yml +#- import_playbook: group_tasks/mail.yml # gather_facts getent passwd is broken when importing from here +#- import_playbook: group_tasks/wireguard.yml +#- import_playbook: tor.yml +#- import_playbook: group_tasks/observability/main.yml # Pending: https://stackoverflow.com/q/77318959/9290 +#- import_playbook: group_tasks/postgresql.yml +#- import_playbook: group_tasks/strfry.yml # role always builds, even when we don't need it to. Haven't made a workaround yet. +- import_playbook: host_tasks/gabite.bitcoiner.social/main.yml +#- import_playbook: host_tasks/garchomp.bitcoiner.social/strfry.yml +#- import_playbook: host_tasks/garchomp.bitcoiner.social/hablanews.yml \ No newline at end of file diff --git a/ansible/playbooks/node_exporter.yml b/ansible/playbooks/node_exporter.yml new file mode 100644 index 0000000..1673b5a --- /dev/null +++ b/ansible/playbooks/node_exporter.yml @@ -0,0 +1,7 @@ +--- +- hosts: all + become: yes + + roles: + - role: prometheus.prometheus.node_exporter + tags: node_exporter diff --git a/ansible/playbooks/oneshots/README.md b/ansible/playbooks/oneshots/README.md new file mode 100644 index 0000000..758d535 --- /dev/null +++ b/ansible/playbooks/oneshots/README.md @@ -0,0 +1,11 @@ +# oneshots + +Quick and dirty setup scripts for bare metal machines and cheap VMs. + +## doas + +```bash +ansible-playbook oneshots/doas/main.yml -e "ansible_user=root" --limit +``` + +* This is a oneshot because changing from sudo to doas midflight requires the inventory file to be changed. diff --git a/ansible/playbooks/oneshots/doas/main.yml b/ansible/playbooks/oneshots/doas/main.yml new file mode 100644 index 0000000..6775fd9 --- /dev/null +++ b/ansible/playbooks/oneshots/doas/main.yml @@ -0,0 +1,40 @@ +--- +- hosts: all + + tasks: + - name: "Ensure normal user exists called: {{ sysadmin_username }}." + ansible.builtin.user: + name: "{{ sysadmin_username }}" + shell: /bin/bash + register: new_sysadmin + + - name: Copy ssh directory + ansible.builtin.copy: + remote_src: true + src: /root/.ssh + dest: "/home/{{ sysadmin_username }}/" + owner: "{{ sysadmin_username }}" + group: "{{ sysadmin_username }}" + when: new_sysadmin.changed + + - name: Ensure doas is installed. + ansible.builtin.package: + name: doas + state: present + + - name: Configure doas. + ansible.builtin.template: + src: doas.conf.j2 + dest: /etc/doas.conf + +# Ubuntu requires setting a root password before `sudo` can be removed. +# - name: Try removing sudo. +# ansible.builtin.package: +# name: sudo +# state: absent +# tags: test +# environment: +# SUDO_FORCE_REMOVE: yes + +- import_playbook: ../../ssh.yml +- import_playbook: ../../linux.yml \ No newline at end of file diff --git a/ansible/playbooks/oneshots/doas/templates/doas.conf.j2 b/ansible/playbooks/oneshots/doas/templates/doas.conf.j2 new file mode 100644 index 0000000..e775654 --- /dev/null +++ b/ansible/playbooks/oneshots/doas/templates/doas.conf.j2 @@ -0,0 +1,5 @@ +permit :wheel +permit nopass root +{% if sysadmin_username is defined %} +permit nopass {{ sysadmin_username }} +{% endif %} \ No newline at end of file diff --git a/ansible/playbooks/ssh.yml b/ansible/playbooks/ssh.yml new file mode 100644 index 0000000..d37dca2 --- /dev/null +++ b/ansible/playbooks/ssh.yml @@ -0,0 +1,40 @@ +--- +- hosts: all + become: true + + handlers: + - name: restart ssh + service: name=sshd state=restarted + + tasks: + - name: Configure sshd to read from authorized_keys.d + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^AuthorizedKeysFile.*$' + line: AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u + notify: restart ssh + + - name: Ensure authorized_keys.d + ansible.builtin.file: + path: /etc/ssh/authorized_keys.d + state: directory + + - name: Configure authorized keys + ansible.builtin.copy: + src: ~/.ssh/ansible_sysadmin_keys + dest: "/etc/ssh/authorized_keys.d/{{ sysadmin_username }}" + owner: "{{ sysadmin_username }}" + group: "{{ sysadmin_username }}" + + - name: Ensure root ssh directory + ansible.builtin.file: + path: /root/.ssh + state: directory + mode: '0700' + + - name: Configure authorized keys for root + ansible.builtin.copy: + src: ~/.ssh/ansible_root_keys + dest: /root/.ssh/authorized_keys + owner: root + group: root \ No newline at end of file diff --git a/ansible/playbooks/tor.yml b/ansible/playbooks/tor.yml new file mode 100644 index 0000000..c4dba69 --- /dev/null +++ b/ansible/playbooks/tor.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + roles: + # - role: bleetube.tor + # tags: onion + - role: systemli.onion + tags: onion + become: yes diff --git a/ansible/requirements.txt b/ansible/requirements.txt new file mode 100644 index 0000000..fd95e48 --- /dev/null +++ b/ansible/requirements.txt @@ -0,0 +1,3 @@ +# https://github.com/grafana/grafana-ansible-collection/blob/a2a8f423d64b0507d3d0bc7fb4dd3ee4628de1f7/roles/grafana/README.md?plain=1#L14 +# required by grafana.grafana.grafana, and caddy-ansible/caddy +jmespath diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..bb3e4f6 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,78 @@ +--- +collections: + + # Warning: does not consistently bring in the latest version of the nginx_conf role + - name: nginxinc.nginx_core + src: https://github.com/nginxinc/ansible-collection-nginx + version: '>=0.6.0' + + - name: prometheus.prometheus + src: https://github.com/prometheus-community/ansible +# version: '>=0.5.0' + + - name: grafana.grafana + src: https://github.com/grafana/grafana-ansible-collection +# version: '>=2.1.4' + +roles: + + - name: bleetube.dotfiles + src: https://git.satstack.dev/blee/ansible-role-dotfiles/archive/main.tar.gz + + - name: bleetube.linux + src: https://git.satstack.dev/blee/ansible-role-linux/archive/main.tar.gz + + - name: bleetube.lego + src: https://git.satstack.dev/blee/ansible-role-lego/archive/main.tar.gz + + - name: bleetube.wireguard + src: https://git.satstack.dev/blee/ansible-role-wireguard/archive/main.tar.gz + + - name: bleetube.disposable-mail + src: https://git.satstack.dev/blee/ansible-role-disposable-mail/archive/main.tar.gz + + - name: bleetube.ntfy + src: https://git.satstack.dev/blee/ansible-role-ntfy/archive/main.tar.gz + + - name: bleetube.ntfy-alertmanager + src: https://git.satstack.dev/blee/ansible-role-ntfy-alertmanager/archive/main.tar.gz + + - name: bleetube.nodejs + src: https://git.satstack.dev/blee/ansible-role-nodejs/archive/main.tar.gz + + - name: bleetube.strfry + src: https://git.satstack.dev/blee/ansible-role-strfry/archive/main.tar.gz + + - name: bleetube.snort + src: https://git.satstack.dev/blee/ansible-role-snort/archive/main.tar.gz + + # includes a minor change that skips the outdated apt role dependency +# - src: https://github.com/bleetube/ansible-role-onion +# name: systemli.onion +# version: skip_apt_key +# version: '>=2.3.0' + - src: https://github.com/systemli/ansible-role-onion + name: systemli.onion + + - name: geerlingguy.certbot + src: https://github.com/geerlingguy/ansible-role-certbot + + - src: https://github.com/gantsign/ansible-role-golang + name: gantsign.golang + + - src: https://github.com/ANXS/postgresql + name: anxs.postgresql +# version: v1.14.1 + + - src: https://github.com/nginxinc/ansible-role-nginx + name: nginx_core.nginx + + - src: https://github.com/nginxinc/ansible-role-nginx-config + name: nginx_core.nginx_config + #version: '>=0.7.0' + + - name: caddy + src: https://github.com/caddy-ansible/caddy-ansible + + - name: robertdebock.dovecot + src: https://github.com/robertdebock/ansible-role-dovecot diff --git a/dnscontrol/creds.json b/dnscontrol/creds.json new file mode 100644 index 0000000..ad0ab49 --- /dev/null +++ b/dnscontrol/creds.json @@ -0,0 +1,10 @@ +{ + "bind": { + "TYPE": "BIND" + }, + "namecheap": { + "TYPE": "NAMECHEAP", + "apikey": "$NAMECHEAP_API_KEY", + "apiuser": "$NAMECHEAP_API_USER" + } +} diff --git a/dnscontrol/dnsconfig.js b/dnscontrol/dnsconfig.js new file mode 100644 index 0000000..960f31d --- /dev/null +++ b/dnscontrol/dnsconfig.js @@ -0,0 +1,24 @@ +// https://docs.dnscontrol.org/ +var REG_NAMECHEAP = NewRegistrar('namecheap'); +var DSP_NAMECHEAP = NewDnsProvider("namecheap"); + +D('bitcoiner.social', REG_NAMECHEAP, DnsProvider(DSP_NAMECHEAP), + A('@', '23.137.254.12'), // gabite + A('nostr', '23.137.254.12'), // gabite + A('mail', '74.48.94.75'), // garchomp + A('garchomp', '74.48.94.75'), + A('gabite', '23.137.254.12'), + AAAA('gabite', '2602:fc24:10:9::1'), +// A('www', '23.95.61.214'), // porygon + CNAME('www', 'bitcoiner.social.'), + MX('@', 10, 'mail.bitcoiner.social.'), + TXT('@', 'v=spf1 mx ~all'), + TXT('mail._domainkey', 'v=DKIM1; h=sha256; k=rsa; s=email; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYynQ/ytpvm/JpR5G2Dr4Z8pE25x42tzwHflXgIHBwWT25tQQER9C/IKRa78fNQ1kkkhyzM1kT18m62rYJH1l1OGfKmC8gmfw8feIFggo4h6F2pUw8/6+4v5ZCs+heT7HNHonnOpSjvAAHv27W1PjMn+aoY9c49qQuxkSo9xBW4MA4wODUkWA2S4gmfDZwKmxZcgQBqvo+vSiY0Pv4eRe9yNKH4ADY13dYR6dJVRmWjX7yvvkOdQ3t/jChjYiu6dYkz3B/hj2Z5M/A0DGcgXuIo5m5QzI539kp878um2bB1gHICGVSB2zn+LvCzBm/Xp/jqEs3O/08HQls2BSbGHgwIDAQAB'), + TXT('_dmarc', 'v=DMARC1; p=reject; rua=mailto:dmarc@bitcoiner.social') +); + +D('bitcoiner.wiki', REG_NAMECHEAP, DnsProvider(DSP_NAMECHEAP), + A('@', '74.48.94.75'), // garchomp + CNAME('www', 'bitcoiner.wiki.'), + TXT('@', 'v=spf1 mx ~all') +); diff --git a/docs/decisions.md b/docs/decisions.md new file mode 100644 index 0000000..9c14136 --- /dev/null +++ b/docs/decisions.md @@ -0,0 +1,24 @@ +# Architectural Decision Record + +Servers are named after Pokemon because [names should be cute, not descriptive](https://news.ycombinator.com/item?id=34320517). + +## DNS + +DNS provider is Namecheap because they provide the following reasons: + +* DNS can be managed via dnscontrol +* Invoices can be paid to Namecheap directly without any intermediaries via their btcpayserver + +I had also tried EasyDNS, but while their DNS could be managed via OctoDNS, I found it poorly supported. Their invoices could be paid in Bitcoin, but only through a third party. + +## Web Proxy + +Nginx for nostr relays because it provides peak performance and allows for complex configuration. Otherwise caddy is preferred for its configuration simplicity which reduces the total cost of ownership. The two web proxies cannot co-exist on the same server with a single public IP address, since both use http-01 validation with nginx and they cannot share port 80. + +## TLS + +* Certbot is used for nostr relay certificate renewal with http-01 validation because it is reliable and works in our nginx deployment. +* ACME lego is used to renew mail server certificates with dns-01 validation because it doesn't need a webserver. +* Caddy is used to run web services and automate certificate renewal on most other servers because of its configuration simplicity and reasonable performance. + +Our nginx configuration continues to include TLS1.2 in addition to TLS1.3 to support legacy clients. It should probably be removed soon, as that would remove the need to generate dhparams. diff --git a/scripts/backups/batch.sh b/scripts/backups/batch.sh new file mode 100755 index 0000000..822db19 --- /dev/null +++ b/scripts/backups/batch.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -x +set -e +HOSTS=( + garchomp.bitcoiner.social + gabite.bitcoiner.social +) + +#source functions.sh +for hostname in ${HOSTS[@]}; do + echo "Running script for $hostname" + (exec ./hosts/${hostname}.sh) + echo "Finished running script for $hostname" +done + +du -sh $HOME/archive diff --git a/scripts/backups/functions.sh b/scripts/backups/functions.sh new file mode 100644 index 0000000..e4a542e --- /dev/null +++ b/scripts/backups/functions.sh @@ -0,0 +1,45 @@ +TIMESTAMP=$(date +%m-%d-%Y) +TIMESTAMP=$(date +%Y-%m-%d) +BACKUP_PG_DB() { + BACKUP_DIR=$HOME/archive/${TARGET}/postgresql + DUMP_FILE=/var/lib/postgresql/${1}_${TIMESTAMP}.dump.bz2 + ssh root@${TARGET} "cd /var/lib/postgresql && doas -u postgres /usr/bin/pg_dump -Fc ${1} | /usr/bin/bzip2 > ${DUMP_FILE}" + mkdir -p ${BACKUP_DIR} + rsync -tav root@${TARGET}:${DUMP_FILE} ${BACKUP_DIR}/ + ssh root@${TARGET} rm -v ${DUMP_FILE} + + # Only remove older backups if newer backups exist + NEWER_BACKUPS=$(find $BACKUP_DIR -mtime -60 -type f -name "${1}_*.dump.bz2") + if [[ -n $NEWER_BACKUPS ]]; then + find $BACKUP_DIR -mtime +60 -type f -name "${1}_*.dump.bz2" -delete + else + echo "No newer backups found. Old backups not pruned." + fi +} +BACKUP_MAIL() { + mkdir -p $HOME/archive/${TARGET}/{dovecot,postfix} + rsync -tav root@${TARGET}:/etc/dovecot/{imap.passwd,dovecot.conf} $HOME/archive/${TARGET}/dovecot/ + rsync -tav root@${TARGET}:/etc/postfix/virtual $HOME/archive/${TARGET}/postfix + rsync -tav root@${TARGET}:/etc/dkimkeys $HOME/archive/${TARGET}/ + rsync -tav root@${TARGET}:/var/vmail $HOME/archive/${TARGET}/ + ssh root@${TARGET} find "/var/vmail/*/main/cur/" -type f -mtime +90 -delete +} +BACKUP_STRFRY_DB() { + BACKUP_DIR=$HOME/archive/${TARGET}/strfry + DUMP_FILE=/tmp/strfry_${TIMESTAMP}.jsonl.zst + + # Only export data since the last backup, if any exist + pushd ${BACKUP_DIR} + LAST_BACKUP_DATE=$(ls strfry_*.jsonl.zst | sort -t_ -k2 | tail -n1 | sed -E 's/strfry_([0-9-]+).jsonl.zst/\1/') + if [[ -n $LAST_BACKUP_DATE ]]; then + LAST_BACKUP_TIMESTAMP=$(date -d "${LAST_BACKUP_DATE}" +%s) + EXPORT_SINCE="--since ${LAST_BACKUP_TIMESTAMP}" + fi + popd + + ssh root@${TARGET} "cd /var/lib/strfry && doas -u strfry strfry export ${EXPORT_SINCE} | zstd -c > ${DUMP_FILE}" + + mkdir -p ${BACKUP_DIR} + rsync -taP root@${TARGET}:${DUMP_FILE} ${BACKUP_DIR} + ssh root@${TARGET} rm -v ${DUMP_FILE} +} \ No newline at end of file diff --git a/scripts/backups/hosts/gabite.bitcoiner.social.sh b/scripts/backups/hosts/gabite.bitcoiner.social.sh new file mode 100755 index 0000000..929f8ab --- /dev/null +++ b/scripts/backups/hosts/gabite.bitcoiner.social.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -x +TARGET=$(basename -- "$0" .sh) + +source "$(dirname "$0")/../functions.sh" + +BACKUP_MAIL +BACKUP_STRFRY_DB + +# handle the strfry database separately +rsync -tav --exclude venv --exclude plugin.log --exclude data.mdb root@${TARGET}:/var/lib/strfry $HOME/archive/${TARGET}/ +rsync -tav root@${TARGET}:/var/lib/strfry/{pubkeys.*} $HOME/archive/${TARGET}/stfry/ +rsync -tav root@${TARGET}:/var/www/static/attachments $HOME/archive/${TARGET}/ + +# nostdress-wireguard +mkdir -p $HOME/archive/${TARGET}/wireguard +rsync -tav root@${TARGET}:/etc/wireguard/lanturn.conf $HOME/archive/${TARGET}/wireguard/ + +# tor +rsync -tav root@${TARGET}:/var/lib/tor $HOME/archive/${TARGET}/ +rsync -tav root@${TARGET}:/etc/tor/torrc $HOME/archive/${TARGET}/ + +du -sh $HOME/archive/${TARGET}/ \ No newline at end of file diff --git a/scripts/backups/hosts/garchomp.bitcoiner.social.sh b/scripts/backups/hosts/garchomp.bitcoiner.social.sh new file mode 100755 index 0000000..2211e10 --- /dev/null +++ b/scripts/backups/hosts/garchomp.bitcoiner.social.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -x +TARGET=$(basename -- "$0" .sh) + +source "$(dirname "$0")/../functions.sh" + +BACKUP_MAIL +BACKUP_STRFRY_DB + +# handle the strfry database separately +rsync -tav --exclude venv --exclude plugin.log --exclude data.mdb root@${TARGET}:/var/lib/strfry $HOME/archive/${TARGET}/ +rsync -tav root@${TARGET}:/var/www/static/attachments $HOME/archive/${TARGET}/ + +du -sh $HOME/archive/${TARGET}/ \ No newline at end of file