diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2025-10-11 15:36:54 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2025-10-11 15:36:54 +0300 |
| commit | 3117429d1eb122382e78e6cb27a07dd878f7c0e9 (patch) | |
| tree | 9678c0a90c36f869a5152fc2d79571e23106428b /templates/ingress | |
| parent | e4faedce00a921ab3b58b816eda987feaa2f0e9e (diff) | |
Improve ingress
Diffstat (limited to 'templates/ingress')
| -rw-r--r-- | templates/ingress/etc_crontab.j2 | 5 | ||||
| -rw-r--r-- | templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 | 151 | ||||
| -rw-r--r-- | templates/ingress/usr_local_etc_hetzner_auth.j2 | 1 | ||||
| -rw-r--r-- | templates/ingress/usr_local_etc_nginx_nginx.conf.j2 | 19 |
4 files changed, 176 insertions, 0 deletions
diff --git a/templates/ingress/etc_crontab.j2 b/templates/ingress/etc_crontab.j2 index 0ec694d..8fd559e 100644 --- a/templates/ingress/etc_crontab.j2 +++ b/templates/ingress/etc_crontab.j2 @@ -10,3 +10,8 @@ PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin {% for item in items %} {{ loop.index0 }} 2 * * * root certbot certonly --nginx -n -m jan@jantuomi.fi --agree-tos -d {{ item }} {% endfor %} + +# Update dynamic DNS (Hetzner) +{% for elem in hetzner_zone_record_ids %} +* * * * * root /usr/local/bin/hetzner_ddns.sh --zone-id '{{ elem.zone_id }}' --record-id '{{ elem.record_id }}' --record-name '{{ elem.name }}' --record-type '{{ elem.type }}' --ttl '{{ elem.ttl }}' --iface-cmd 'ifconfig epw1b' >>/var/log/hetzner_ddns.log 2>&1 +{% endfor %} diff --git a/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 b/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 new file mode 100644 index 0000000..3b23ea5 --- /dev/null +++ b/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 @@ -0,0 +1,151 @@ +#!/bin/sh +# +# Hetzner DNS record updater (one-shot for cron) +# +# Adds named CLI args: +# --zone-id --record-id --record-name +# --record-type --ttl --iface-cmd +# +# Env overrides (with sensible defaults): +# API_TOKEN_FILE (default: /usr/local/etc/hetzner_auth) +# API_BASE (default: https://dns.hetzner.com/api/v1) +# +# Exit codes: +# 0 = Updated or already up-to-date +# 1 = Transient issue (no local IP, API/network) +# 2 = Config error (bad/missing args, token) +# +# Requirements: curl, awk + +set -eu + +# ---------------------------- Defaults --------------------------------------- +# Env-overridable: +: "${API_TOKEN_FILE:=/usr/local/etc/hetzner_auth}" +: "${API_BASE:=https://dns.hetzner.com/api/v1}" +# ----------------------------------------------------------------------------- + + +usage() { + cat <<'USAGE' >&2 +Usage: hetzner_ddns.sh [OPTIONS] + +Options (named): + --zone-id ID Hetzner zone ID + --record-id ID Record ID to update + --record-name NAME Record name (e.g., "host" part) + --record-type TYPE Record type (e.g., A, AAAA, TXT) + --ttl SECONDS TTL in seconds + --iface-cmd CMD Command that prints interface info (for IP discovery) + -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 DNS API base URL + (default: https://dns.hetzner.com/api/v1) + +Examples: + API_TOKEN_FILE=/secret/token \ + ./hetzner_ddns.sh \ + --zone-id ZONE123 --record-id REC456 \ + --record-name example --record-type A \ + --ttl 300 --iface-cmd "ifconfig em0" +USAGE +} + +# ----------------------------- Arg parsing ----------------------------------- +# Accept both `--key value` and `--key=value` +while [ $# -gt 0 ]; do + case "$1" in + --zone-id=*) ZONE_ID=${1#*=} ;; + --zone-id) ZONE_ID=$2; shift ;; + --record-id=*) RECORD_ID=${1#*=} ;; + --record-id) RECORD_ID=$2; shift ;; + --record-name=*) RECORD_NAME=${1#*=} ;; + --record-name) RECORD_NAME=$2; shift ;; + --record-type=*) RECORD_TYPE=${1#*=} ;; + --record-type) RECORD_TYPE=$2; shift ;; + --ttl=*) TTL=${1#*=} ;; + --ttl) TTL=$2; shift ;; + --iface-cmd=*) IFACE_CMD=${1#*=} ;; + --iface-cmd) IFACE_CMD=$2; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; break ;; + -*) + echo "ERROR: Unknown option: $1" >&2 + usage + exit 2 + ;; + *) + echo "ERROR: Unexpected positional argument: $1" >&2 + usage + exit 2 + ;; + esac + shift +done + +fail() { echo "ERROR: $*" >&2; exit 2; } +warn() { echo "WARN: $*" >&2; } +info() { echo "INFO: $*"; } + +# ----------------------------- Validation ------------------------------------ +[ -n "${ZONE_ID}" ] || fail "Missing --zone-id" +[ -n "${RECORD_ID}" ] || fail "Missing --record-id" +[ -n "${RECORD_NAME}" ] || fail "Missing --record-name" +[ -n "${RECORD_TYPE}" ] || fail "Missing --record-type" +[ -n "${TTL}" ] || fail "Missing --ttl" +case "${TTL}" in (*[!0-9]*|'') fail "--ttl must be an integer";; esac +[ -n "${IFACE_CMD}" ] || fail "Missing --iface-cmd" + +# ------------------------------ Auth ----------------------------------------- +if [ ! -r "${API_TOKEN_FILE}" ]; then + echo "ERROR: Token file missing or unreadable: ${API_TOKEN_FILE}" >&2 + exit 2 +fi +API_TOKEN="$(cat "${API_TOKEN_FILE}" | tr -d '[:space:]')" +[ -n "${API_TOKEN}" ] || { echo "ERROR: API token is empty" >&2; exit 2; } + +# ------------------------------ 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 '{"type":"%s","name":"%s","value":"%s","zone_id":"%s","ttl":%s}' \ + "${RECORD_TYPE}" "${RECORD_NAME}" "${IFACE_IP}" "${ZONE_ID}" "${TTL}") + +HTTP_CODE=$( + curl -sS -o /dev/null -w "%{http_code}" -X PUT \ + -H "Content-Type: application/json" \ + -H "Auth-API-Token: ${API_TOKEN}" \ + --data "${BODY}" \ + "${API_BASE}/records/${RECORD_ID}" +) + +case "${HTTP_CODE}" in + 200) + info "Updated zone ${ZONE_ID} record ${RECORD_ID} -> ${RECORD_TYPE} ${RECORD_NAME} ${IFACE_IP} with TTL ${TTL}" + exit 0 + ;; + 4*) + echo "ERROR: Client error from API (HTTP ${HTTP_CODE}) - check IDs/token/body" >&2 + 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 new file mode 100644 index 0000000..129dccf --- /dev/null +++ b/templates/ingress/usr_local_etc_hetzner_auth.j2 @@ -0,0 +1 @@ +{{ hetzner_pat }} diff --git a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 b/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 index 9f20a5f..04c43db 100644 --- a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 +++ b/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 @@ -76,6 +76,25 @@ http { } {% endif -%} + {# TODO: parameterize this properly -#} + server { + server_name jantuomi.fi; + http2 on; + + 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; + + ssl_certificate /usr/local/etc/letsencrypt/live/jantuomi.fi/fullchain.pem; + ssl_certificate_key /usr/local/etc/letsencrypt/live/jantuomi.fi/privkey.pem; + include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; + + return 307 https://jan.systems$request_uri; + } + {% endfor -%} {% for jail in jails -%} server { |
