Initialize newly refactored infra repo.

This commit is contained in:
Jim 2024-06-15 10:09:21 -07:00
commit d867522fbe
77 changed files with 2241 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -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

15
ansible/README.md Normal file
View File

@ -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"
```

11
ansible/ansible.cfg Normal file
View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -0,0 +1,6 @@
--- # This variable is used by:
# playbooks/podman.yml
# playbooks/ssh.yml
# roles/bleetube.dotfiles
sysadmin_username: jim

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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:

View File

@ -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') }}"

View File

@ -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"

View File

@ -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: ""

View File

@ -0,0 +1,2 @@
--- # bleetube.disposable-mail role variables
postfix_domain: bitcoiner.social

View File

@ -0,0 +1,4 @@
---
sysadmin_packages_custom:
- wireguard
- wireguard-tools

View File

@ -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

View File

@ -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 }

View File

@ -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
}

View File

@ -0,0 +1,2 @@
--- # bleetube.disposable-mail role variables
postfix_domain: bitcoiner.social

View File

@ -0,0 +1,5 @@
---
# permit nopass blee as postgres
doas_permit_list:
- username: blee
role: opendkim

View File

@ -0,0 +1,3 @@
# habla.news requirements
npm_packages:
- pnpm

View File

@ -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

View File

@ -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

16
ansible/hosts.cfg Normal file
View File

@ -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

View File

@ -0,0 +1,5 @@
---
- hosts: all
roles:
- role: caddy
become: yes

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
---
- hosts: nginx, certbot
roles:
- role: nginxinc.nginx_core.nginx
tags: nginx
become: yes

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
- hosts: strfry
roles:
- role: bleetube.strfry
tags: strfry

View File

@ -0,0 +1,7 @@
---
- hosts: wireguard
roles:
- role: bleetube.wireguard
become: true
tags: wireguard

View File

@ -0,0 +1,9 @@
# Manual steps
Not here:
* `/etc/wireguard/lanturn.conf`
* `/var/lib/strfry/` stuff:
* pubkeys.db
* pubkeys.banned.ts
* pubkeys.premium.json

View File

@ -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 *;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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"}

View File

@ -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"]}}

View File

@ -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()

View File

@ -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

View File

@ -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<number, number> = {
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<void> => {
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();

View File

@ -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.

View File

@ -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

View File

@ -0,0 +1,9 @@
[Unit]
Description=Compact and defragment strfry database
[Timer]
# Every Sunday
OnCalendar=Sun *-*-* 1:00:00
[Install]
WantedBy=default.target

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
---
- hosts: all
roles:
- role: bleetube.linux
tags: linux
become: yes
- role: bleetube.dotfiles
tags: dotfiles

View File

@ -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

View File

@ -0,0 +1,7 @@
---
- hosts: all
become: yes
roles:
- role: prometheus.prometheus.node_exporter
tags: node_exporter

View File

@ -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 <host>
```
* This is a oneshot because changing from sudo to doas midflight requires the inventory file to be changed.

View File

@ -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

View File

@ -0,0 +1,5 @@
permit :wheel
permit nopass root
{% if sysadmin_username is defined %}
permit nopass {{ sysadmin_username }}
{% endif %}

40
ansible/playbooks/ssh.yml Normal file
View File

@ -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

View File

@ -0,0 +1,8 @@
---
- hosts: all
roles:
# - role: bleetube.tor
# tags: onion
- role: systemli.onion
tags: onion
become: yes

3
ansible/requirements.txt Normal file
View File

@ -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

78
ansible/requirements.yml Normal file
View File

@ -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

10
dnscontrol/creds.json Normal file
View File

@ -0,0 +1,10 @@
{
"bind": {
"TYPE": "BIND"
},
"namecheap": {
"TYPE": "NAMECHEAP",
"apikey": "$NAMECHEAP_API_KEY",
"apiuser": "$NAMECHEAP_API_USER"
}
}

24
dnscontrol/dnsconfig.js Normal file
View File

@ -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')
);

24
docs/decisions.md Normal file
View File

@ -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.

16
scripts/backups/batch.sh Executable file
View File

@ -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

View File

@ -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}
}

View File

@ -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}/

View File

@ -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}/