diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-05-13 00:13:57 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-05-16 18:42:27 +0300 |
| commit | b5860daf11ac353049cb1654b9414a129e5cfb96 (patch) | |
| tree | 87ed89711e4f0e85ace0a97fa123199152c67302 /templates/ingress | |
| parent | 4715a28fdcd87440400d17154bfa361d99db29cc (diff) | |
Rework
Diffstat (limited to 'templates/ingress')
| -rwxr-xr-x | templates/ingress/acme-dns-auth.py | 170 | ||||
| -rw-r--r-- | templates/ingress/etc_crontab.j2 | 18 | ||||
| -rw-r--r-- | templates/ingress/etc_pf.conf.j2 | 27 | ||||
| -rw-r--r-- | templates/ingress/nginx_snippet_ban.inc | 17 | ||||
| -rw-r--r-- | templates/ingress/pf-ban-socket.py | 72 | ||||
| -rw-r--r-- | templates/ingress/usr_local_bin_gen_goaccess.sh.j2 | 8 | ||||
| -rw-r--r-- | templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 | 130 | ||||
| -rw-r--r-- | templates/ingress/usr_local_etc_hetzner_auth.j2 | 1 | ||||
| -rw-r--r-- | templates/ingress/usr_local_etc_nginx_nginx.conf.j2 | 141 | ||||
| -rw-r--r-- | templates/ingress/usr_local_etc_rc.d_pf_ban_socket | 43 |
10 files changed, 0 insertions, 627 deletions
diff --git a/templates/ingress/acme-dns-auth.py b/templates/ingress/acme-dns-auth.py deleted file mode 100755 index 77928e6..0000000 --- a/templates/ingress/acme-dns-auth.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -# -# Source: https://github.com/joohoi/acme-dns-certbot-joohoi/blob/master/acme-dns-auth.py -# Some modifications to configuration values present. - - -import json -import os -import sys - -import requests - -### EDIT THESE: Configuration values ### - -# URL to acme-dns instance -ACMEDNS_URL = "https://auth.acme-dns.io" -# Path for acme-dns credential storage -STORAGE_PATH = "/usr/local/etc/letsencrypt/acmedns.json" -# Whitelist for address ranges to allow the updates from -# Example: ALLOW_FROM = ["192.168.10.0/24", "::1/128"] -ALLOW_FROM = [] -# Force re-registration. Overwrites the already existing acme-dns accounts. -FORCE_REGISTER = False - -### DO NOT EDIT BELOW THIS POINT ### -### HERE BE DRAGONS ### - -DOMAIN = os.environ["CERTBOT_DOMAIN"] -if DOMAIN.startswith("*."): - DOMAIN = DOMAIN[2:] -VALIDATION_DOMAIN = "_acme-challenge." + DOMAIN -VALIDATION_TOKEN = os.environ["CERTBOT_VALIDATION"] - - -class AcmeDnsClient(object): - """ - Handles the communication with ACME-DNS API - """ - - def __init__(self, acmedns_url): - self.acmedns_url = acmedns_url - - def register_account(self, allowfrom): - """Registers a new ACME-DNS account""" - - if allowfrom: - # Include whitelisted networks to the registration call - reg_data = {"allowfrom": allowfrom} - res = requests.post( - self.acmedns_url + "/register", data=json.dumps(reg_data) - ) - else: - res = requests.post(self.acmedns_url + "/register") - if res.status_code == 201: - # The request was successful - return res.json() - else: - # Encountered an error - msg = ( - "Encountered an error while trying to register a new acme-dns " - "account. HTTP status {}, Response body: {}" - ) - print(msg.format(res.status_code, res.text)) - sys.exit(1) - - def update_txt_record(self, account, txt): - """Updates the TXT challenge record to ACME-DNS subdomain.""" - update = {"subdomain": account["subdomain"], "txt": txt} - headers = { - "X-Api-User": account["username"], - "X-Api-Key": account["password"], - "Content-Type": "application/json", - } - res = requests.post( - self.acmedns_url + "/update", headers=headers, data=json.dumps(update) - ) - if res.status_code == 200: - # Successful update - return - else: - msg = ( - "Encountered an error while trying to update TXT record in " - "acme-dns. \n" - "------- Request headers:\n{}\n" - "------- Request body:\n{}\n" - "------- Response HTTP status: {}\n" - "------- Response body: {}" - ) - s_headers = json.dumps(headers, indent=2, sort_keys=True) - s_update = json.dumps(update, indent=2, sort_keys=True) - s_body = json.dumps(res.json(), indent=2, sort_keys=True) - print(msg.format(s_headers, s_update, res.status_code, s_body)) - sys.exit(1) - - -class Storage(object): - def __init__(self, storagepath): - self.storagepath = storagepath - self._data = self.load() - - def load(self): - """Reads the storage content from the disk to a dict structure""" - data = dict() - filedata = "" - try: - with open(self.storagepath, "r") as fh: - filedata = fh.read() - except IOError as e: - if os.path.isfile(self.storagepath): - # Only error out if file exists, but cannot be read - print("ERROR: Storage file exists but cannot be read") - sys.exit(1) - try: - data = json.loads(filedata) - except ValueError: - if len(filedata) > 0: - # Storage file is corrupted - print("ERROR: Storage JSON is corrupted") - sys.exit(1) - return data - - def save(self): - """Saves the storage content to disk""" - serialized = json.dumps(self._data) - try: - with os.fdopen( - os.open(self.storagepath, os.O_WRONLY | os.O_CREAT, 0o600), "w" - ) as fh: - fh.truncate() - fh.write(serialized) - except IOError as e: - print("ERROR: Could not write storage file.") - sys.exit(1) - - def put(self, key, value): - """Puts the configuration value to storage and sanitize it""" - # If wildcard domain, remove the wildcard part as this will use the - # same validation record name as the base domain - if key.startswith("*."): - key = key[2:] - self._data[key] = value - - def fetch(self, key): - """Gets configuration value from storage""" - try: - return self._data[key] - except KeyError: - return None - - -if __name__ == "__main__": - # Init - client = AcmeDnsClient(ACMEDNS_URL) - storage = Storage(STORAGE_PATH) - - # Check if an account already exists in storage - account = storage.fetch(DOMAIN) - if FORCE_REGISTER or not account: - # Create and save the new account - account = client.register_account(ALLOW_FROM) - storage.put(DOMAIN, account) - storage.save() - - # Display the notification for the user to update the main zone - msg = "Please add the following CNAME record to your main DNS zone:\n{}" - cname = "{} CNAME {}.".format(VALIDATION_DOMAIN, account["fulldomain"]) - print(msg.format(cname)) - - # Update the TXT record in acme-dns instance - client.update_txt_record(account, VALIDATION_TOKEN) diff --git a/templates/ingress/etc_crontab.j2 b/templates/ingress/etc_crontab.j2 deleted file mode 100644 index 6879766..0000000 --- a/templates/ingress/etc_crontab.j2 +++ /dev/null @@ -1,18 +0,0 @@ -# /etc/crontab - root's crontab for FreeBSD -# -# -SHELL=/bin/sh -PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin -# -#minute hour mday month wday who command - -# Update LetsEncrypt certificates every day after 2:00 AM -0 2 * * * root certbot certonly -n -m {{ contact_email }} --agree-tos --manual --manual-auth-hook /usr/local/bin/acme-dns-auth.py --preferred-challenges dns --debug-challenges {% for domain in cert_domains %}-d '{{ domain }}' {% endfor %} || echo "ERROR: Failed to renew certs" >&2 - -# Update dynamic DNS (Hetzner) -{% for elem in hetzner_zone_record_ids %} -* * * * * root /usr/local/bin/hetzner_ddns.sh --zone '{{ elem.zone }}' --rr-id '{{ elem.rr_id }}' --iface-cmd 'ifconfig epw1b' >>/var/log/hetzner_ddns.log 2>&1 -{% endfor %} - -# Update goaccess report HTML -0 * * * * root /usr/local/bin/gen_goaccess.sh diff --git a/templates/ingress/etc_pf.conf.j2 b/templates/ingress/etc_pf.conf.j2 deleted file mode 100644 index c0528e1..0000000 --- a/templates/ingress/etc_pf.conf.j2 +++ /dev/null @@ -1,27 +0,0 @@ -{% for jail in jails -%} -{% if jail.name == 'ingress' -%} -# Interfaces & nets -lan = "epl{{ jail.num }}b" -wan = "epw{{ jail.num }}b" -lan_net = "{{ lan_ipv4_network }}" - -table <blocked> persist - -# Keep PF out of loopback, drop by default if you add blocks later -set skip on lo0 -set block-policy drop - -# NAT: translate LAN traffic to the WAN interface address -nat on $wan from $lan_net to any -> ($wan) - -# Block traffic from IPs in the blocked table -block in quick from <blocked> to any - -# Allow all outbound traffic from the jail and LAN via both interfaces -# NAT will be applied automatically when source is in $lan_net and going out $wan -pass out on $wan all keep state -pass out on $lan all keep state - -pass in on $wan inet proto {tcp, udp} from any to any port {80, 443} keep state -{% endif %} -{% endfor %} diff --git a/templates/ingress/nginx_snippet_ban.inc b/templates/ingress/nginx_snippet_ban.inc deleted file mode 100644 index 029b965..0000000 --- a/templates/ingress/nginx_snippet_ban.inc +++ /dev/null @@ -1,17 +0,0 @@ -# Trap route -location ^~ /wp-admin/ { - proxy_set_header X-IP $remote_addr; - proxy_method POST; - proxy_pass http://unix:/var/run/pfban/ban.sock:/ban; - - proxy_connect_timeout 50ms; - proxy_send_timeout 50ms; - proxy_read_timeout 50ms; - - # If the socket isn't up yet, still return something - error_page 500 502 503 504 = @ban_fallback; -} - -location @ban_fallback { - return 204; -} diff --git a/templates/ingress/pf-ban-socket.py b/templates/ingress/pf-ban-socket.py deleted file mode 100644 index cbcd80e..0000000 --- a/templates/ingress/pf-ban-socket.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -import http.server -import os -import re -import socketserver -import subprocess - -SOCK_PATH = "/var/run/pfban/ban.sock" -PF_TABLE = "blocked" - -# Simple, conservative filter to avoid junk / injection -IP_RE = re.compile(r"^[0-9A-Fa-f:.]{3,}$") - - -def ensure_socket_dir(path: str) -> None: - os.makedirs(os.path.dirname(path), mode=0o755, exist_ok=True) - - -class BanHandler(http.server.BaseHTTPRequestHandler): - # Silence default logging - def log_message(self, format, *args): - return - - def do_POST(self): - if self.path != "/ban": - self.send_response(404) - self.end_headers() - return - - ip = self.headers.get("X-IP", "").strip() - - if ip and IP_RE.match(ip): - subprocess.run( - ["/sbin/pfctl", "-t", PF_TABLE, "-T", "add", ip], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) - - self.send_response(204) - self.end_headers() - - def do_GET(self): - self.send_response(405) - self.end_headers() - - -class ThreadingUnixHTTPServer( - socketserver.ThreadingMixIn, - socketserver.UnixStreamServer, -): - daemon_threads = True - - -def main() -> None: - ensure_socket_dir(SOCK_PATH) - - # Remove stale socket if present - try: - os.unlink(SOCK_PATH) - except FileNotFoundError: - pass - - with ThreadingUnixHTTPServer(SOCK_PATH, BanHandler) as httpd: - # Allow nginx workers (www) to connect - os.chmod(SOCK_PATH, 0o660) - - httpd.serve_forever() - - -if __name__ == "__main__": - main() diff --git a/templates/ingress/usr_local_bin_gen_goaccess.sh.j2 b/templates/ingress/usr_local_bin_gen_goaccess.sh.j2 deleted file mode 100644 index 2cfc93a..0000000 --- a/templates/ingress/usr_local_bin_gen_goaccess.sh.j2 +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -TARGET=/mnt/www_goaccess/index.html - -cat /var/log/nginx/access.log | awk '$8=$1$8' | /usr/local/bin/goaccess --log-format=VCOMBINED -j "$(($(nproc) + 1))" --keep-last=30 -a -o $TARGET --restore --persist -chmod 644 $TARGET -chown www $TARGET -chgrp www $TARGET diff --git a/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 b/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 deleted file mode 100644 index a2f4430..0000000 --- a/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/sh -# -# Hetzner DNS record updater (one-shot for cron) -# Requirements: curl, awk - -set -eu - -# ---------------------------- Defaults --------------------------------------- -# Env-overridable: -: "${API_TOKEN_FILE:=/usr/local/etc/hetzner_auth}" -: "${API_BASE:=https://api.hetzner.cloud/v1}" -# ----------------------------------------------------------------------------- - -# Defaults -TTL=300 - -usage() { - cat <<'USAGE' >&2 -Usage: hetzner_ddns.sh [OPTIONS] - -Options (named): - --zone NAME_OR_ID Hetzner Zone name or ID (primary mode only) - --rr-id ID RRSet identifier in the form "rr-name/rr-type" (e.g., "host/A") - --iface-cmd CMD Command that prints interface info (for IP discovery) - --ttl TTL Time-to-live of the record (optional) - -h, --help Show this help - -Environment: - API_TOKEN_FILE Path to file containing ONLY the API token - (default: /usr/local/etc/hetzner_auth) - API_BASE Hetzner Cloud DNS API base URL - (default: https://api.hetzner.cloud/v1) - -Examples: - API_TOKEN_FILE=/secret/token \ - ./hetzner_ddns.sh \ - --zone example.com --rr-name host --rr-type A \ - --iface-cmd "ifconfig em0" -USAGE -} - -ts() { date +"%Y-%m-%dT%H:%M:%S%z"; } -fail() { echo "$(ts) ERROR: $*" >&2; exit 2; } -error() { echo "$(ts) ERROR: $*" >&2; } -warn() { echo "$(ts) WARN: $*" >&2; } -info() { echo "$(ts) INFO: $*"; } - -# ----------------------------- Arg parsing ----------------------------------- -# Accept both `--key value` and `--key=value` - -while [ $# -gt 0 ]; do - case "$1" in - --zone=*) ZONE=${1#*=} ;; - --zone) ZONE=$2; shift ;; - --rr-id=*) RR_ID=${1#*=} ;; - --rr-id) RR_ID=$2; shift ;; - --iface-cmd=*) IFACE_CMD=${1#*=} ;; - --iface-cmd) IFACE_CMD=$2; shift ;; - --ttl=*) TTL=${1#*=} ;; - --ttl) TTL=$2; shift ;; - -h|--help) usage; exit 0 ;; - --) shift; break ;; - -*) - error "Unknown option: $1" - usage - exit 2 - ;; - *) - error "Unexpected positional argument: $1" - usage - exit 2 - ;; - esac - shift -done - -# ----------------------------- Validation ------------------------------------ -[ -n "${ZONE:-}" ] || fail "Missing --zone" -[ -n "${RR_ID:-}" ] || fail "Missing --rr-id" -[ -n "${IFACE_CMD:-}" ] || fail "Missing --iface-cmd" - -# ------------------------------ Auth ----------------------------------------- -if [ ! -r "${API_TOKEN_FILE}" ]; then - fail "Token file missing or unreadable: ${API_TOKEN_FILE}" -fi -API_TOKEN="$(cat "${API_TOKEN_FILE}" | tr -d '[:space:]')" -[ -n "${API_TOKEN}" ] || fail "API token is empty" - -# ------------------------------ Helpers -------------------------------------- -get_ip() { - # Extract first IPv4 after 'inet ' (ignore inet6) - sh -c "${IFACE_CMD}" 2>/dev/null | awk '/(^|[[:space:]])inet[[:space:]]/ {print $2; exit}' -} - -# ------------------------------ Main ----------------------------------------- -IFACE_IP="$(get_ip || true)" -if [ -z "${IFACE_IP}" ]; then - warn "No IPv4 address found via '${IFACE_CMD}' (interface not ready?)" - exit 1 -fi - -BODY=$(printf '{"records":[{"value":"%s","comment":"Updated by hetzner_ddns.sh at %s","ttl":"%s"}]}' \ - "${IFACE_IP}" "$(ts)" "${TTL}") - -HTTP_CODE=$( - curl -sS -o /dev/null -w "%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${API_TOKEN}" \ - --data "${BODY}" \ - "${API_BASE}/zones/${ZONE}/rrsets/${RR_ID}/actions/set_records" -) - -case "${HTTP_CODE}" in - 2*) - info "Updated zone ${ZONE} RRSet ${RR_ID} -> ${IFACE_IP}" - exit 0 - ;; - 4*) - error "Client error from API (HTTP ${HTTP_CODE}) - check zone/rr params/token/body" - exit 2 - ;; - 5*) - warn "Server error from API (HTTP ${HTTP_CODE})" - exit 1 - ;; - *) - warn "Unexpected HTTP status ${HTTP_CODE}" - exit 1 - ;; -esac diff --git a/templates/ingress/usr_local_etc_hetzner_auth.j2 b/templates/ingress/usr_local_etc_hetzner_auth.j2 deleted file mode 100644 index 129dccf..0000000 --- a/templates/ingress/usr_local_etc_hetzner_auth.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ hetzner_pat }} diff --git a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 b/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 deleted file mode 100644 index 16299ef..0000000 --- a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 +++ /dev/null @@ -1,141 +0,0 @@ -worker_processes auto; - -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - - sendfile on; - keepalive_timeout 65; - - log_format vcombined '$host:$server_port ' - '$remote_addr - $remote_user [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent"'; - - access_log /var/log/nginx/access.log vcombined; - - gzip on; - gzip_vary on; - gzip_min_length 512; - gzip_types - text/plain - text/css - application/json - application/javascript - application/xml - image/svg+xml; - - server { - listen 80 default_server; - server_name _; - - include /usr/local/etc/nginx/snippets/ban.inc; - - location / { - return 404; - } - } - - {% for route in ingress_routes -%} - server { - listen 80; - listen [::]:80; - server_name {{ route.host }}; - - include /usr/local/etc/nginx/snippets/ban.inc; - - return 307 https://$host$request_uri; - } - - server { - server_name {{ route.host }}; - http2 on; - - include /usr/local/etc/nginx/snippets/ban.inc; - - listen 443 ssl; - listen [::]:443 ssl; - - # See https://ssl-config.mozilla.org/#server=nginx&version=1.28.0&config=intermediate&openssl=3.4.0&guideline=5.7 - add_header Strict-Transport-Security "max-age=63072000" always; - - # Common hardening headers - add_header X-Content-Type-Options nosniff always; - add_header X-Frame-Options DENY always; - add_header Referrer-Policy strict-origin-when-cross-origin always; - add_header Permissions-Policy interest-cohort=(); - - # Hide "Server: nginx/1.28.0" header - server_tokens off; - - ssl_certificate /usr/local/etc/letsencrypt/live/{{ cert_name }}/fullchain.pem; - ssl_certificate_key /usr/local/etc/letsencrypt/live/{{ cert_name }}/privkey.pem; - include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; - - {% if route.jail is defined -%} - {% for jail in jails if jail.name == route.jail -%} - location / { - proxy_pass http://192.168.2.{{ jail.num }}{% if route.port is defined %}:{{ route.port }}{% endif %}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - {% if jail.name == 'irc_thelounge' -%} - proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; - proxy_set_header Upgrade $http_upgrade; - - # by default nginx times out connections in one minute - proxy_read_timeout 1d; - proxy_send_timeout 1d; - proxy_buffering off; - proxy_request_buffering off; - - client_max_body_size 100M; - {% elif jail.name == 'plex' -%} - proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; - proxy_set_header Upgrade $http_upgrade; - - # Streaming-friendly behavior - proxy_redirect off; - proxy_buffering off; - - # Long streams / slow clients - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - {% endif %} - } - {% endfor %} - {% elif route.redirect is defined -%} - return 307 https://{{ route.redirect }}$request_uri; - {% elif route.ip is defined -%} - location / { - proxy_pass http://{{ route.ip }}:{{ route.port }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # TODO: these shouldn't configured for all ip:port proxies but Immich needs them - proxy_http_version 1.1; - proxy_set_header Connection "upgrade"; - proxy_set_header Upgrade $http_upgrade; - - # by default nginx times out connections in one minute - proxy_read_timeout 1d; - proxy_send_timeout 1d; - proxy_buffering off; - proxy_request_buffering off; - - client_max_body_size 10G; - } - {% endif %} - } - {% endfor %} -} diff --git a/templates/ingress/usr_local_etc_rc.d_pf_ban_socket b/templates/ingress/usr_local_etc_rc.d_pf_ban_socket deleted file mode 100644 index d21bc2d..0000000 --- a/templates/ingress/usr_local_etc_rc.d_pf_ban_socket +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -# -# PROVIDE: pf_ban_socket -# REQUIRE: NETWORKING pf -# KEYWORD: shutdown -# -# Enable in /etc/rc.conf: -# pf_ban_socket_enable="YES" -# -. /etc/rc.subr - -name="pf_ban_socket" -rcvar=pf_ban_socket_enable - -load_rc_config $name - -: ${pf_ban_socket_command:=/usr/local/bin/pf-ban-socket.py} -: ${pf_ban_socket_log:=/var/log/${name}.log} - -start_cmd="${name}_start" -stop_cmd="${name}_stop" -status_cmd="${name}_status" - -extra_commands="status" - -pf_ban_socket_start() { - /usr/local/bin/logto ${pf_ban_socket_log} ${pf_ban_socket_command} & -} - -pf_ban_socket_status() { - if pgrep -f "pf-ban-socket.py"; then - echo "pf_ban_socket is running" - else - echo "pf_ban_socket is not running" - exit 1 - fi -} - -pf_ban_socket_stop() { - pkill "pf-ban-socket.py" -} - -run_rc_command "$1" |
