Last active
February 15, 2026 19:40
-
-
Save OnkelDom/d29a5696553557bdb11bdb60b7896420 to your computer and use it in GitHub Desktop.
Revisions
-
OnkelDom revised this gist
Feb 15, 2026 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -249,10 +249,10 @@ main "$@" ## Create Cronjob To run the script automaticly every 5 minutes, add the following file to `/etc/cron.d/hetzner-cloud-ddns`. ```bash */5 * * * * root /data/hetzner_ddns/hetzner-cloud-ddns.sh /data/hetzner_ddns/hetzner-cloud-ddns.conf >> /var/log/hetzner-ddns-cron.log 2>&1 ``` ----- -
OnkelDom created this gist
Feb 15, 2026 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,286 @@ # Unifi Dream Machine SE Hetzner Cloud API DynDNS This script ist to use dynmic dns updates with the new Hetzner cloud API. Login via ssh to your Dream Machine. **Create Folder**: `mkdir -p /data/hetzner_ddns` Add the following config to `/data/hetzner_ddns/hetzner-cloud-ddns.conf` ```bash # Hetzner Cloud API token HETZNER_API_TOKEN="PASTE_TOKEN_HERE" # IMPORTANT: This is the Zone ID (not the zone name). # Get it via: curl -H "Authorization: Bearer $HETZNER_API_TOKEN" https://api.hetzner.cloud/v1/zones ZONE_ID="YOUR_ZONE_ID" # Comma-separated FQDNs to update (A + AAAA by default) # Dont use fqdn's. The zone name is not needed and adds double entries. RECORD_FQDNS="vpn" # TTL in seconds TTL=86400 # IP discovery (UDM friendly) IPV4_URL="https://ifconfig.io" IPV6_URL="https://ifconfig.io" # Cache directory STATE_DIR="/tmp" # Optional toggles UPDATE_A=1 UPDATE_AAAA=1 ``` Chane persmissions to protect your API key. `chmod 600 /data/hetzner_ddns/hetzner-cloud-ddns.conf` ----- Add the following code to `/data/hetzner_ddns/hetzner-cloud-ddns.sh` ```bash #!/usr/bin/env bash # # Hetzner Cloud DNS Dynamic Update Script (Cloud API) # Updates A + AAAA RRsets with the current public IP address(es) # API Docs: https://docs.hetzner.cloud/reference/cloud#tag/zones # # Usage: ./hetzner-cloud-ddns.sh [config_file] set -euo pipefail # ============ DEFAULT CONFIG PATH ============ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${1:-$SCRIPT_DIR/hetzner-cloud-ddns.conf}" # ============ END CONFIG ============ API_BASE="${API_BASE:-https://api.hetzner.cloud/v1}" LOG_PREFIX="[hetzner-ddns]" log() { echo "$LOG_PREFIX $(date '+%Y-%m-%d %H:%M:%S') $*"; } error() { echo "$LOG_PREFIX $(date '+%Y-%m-%d %H:%M:%S') ERROR: $*" >&2; exit 1; } require_cmd() { command -v "$1" >/dev/null 2>&1 || error "Missing dependency: $1"; } # ============ LOAD CONFIG ============ load_config() { [[ -f "$CONFIG_FILE" ]] || error "Config file not found: $CONFIG_FILE" # shellcheck source=/dev/null source "$CONFIG_FILE" [[ -n "${HETZNER_API_TOKEN:-}" ]] || error "HETZNER_API_TOKEN not set in $CONFIG_FILE" [[ -n "${ZONE_ID:-}" ]] || error "ZONE_ID not set in $CONFIG_FILE (numeric/uuid id from Hetzner Cloud DNS)" [[ -n "${RECORD_FQDNS:-}" ]] || error "RECORD_FQDNS not set in $CONFIG_FILE (e.g. vpn.lenmail.de)" # Optional TTL="${TTL:-86400}" STATE_DIR="${STATE_DIR:-/tmp}" # IP services (UDM friendly) IPV4_URL="${IPV4_URL:-https://ifconfig.io}" IPV6_URL="${IPV6_URL:-https://ifconfig.io}" # Which types to manage UPDATE_A="${UPDATE_A:-1}" UPDATE_AAAA="${UPDATE_AAAA:-1}" mkdir -p "$STATE_DIR" 2>/dev/null || true } # --------- Public IP helpers ---------- get_public_ipv4() { local ip ip="$(curl -4 -fsS --max-time 10 "$IPV4_URL" | tr -d '[:space:]' || true)" [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1 echo "$ip" } get_public_ipv6() { local ip ip="$(curl -6 -fsS --max-time 10 "$IPV6_URL" | tr -d '[:space:]' || true)" # Filter link-local if [[ -z "$ip" ]] || echo "$ip" | grep -qi '^fe80:'; then return 1 fi # Very rough plausibility check (keeps it simple) [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]] || return 1 echo "$ip" } cache_file_for() { local fqdn="$1" local type="$2" # ensure filesystem-safe name echo "${STATE_DIR}/hetzner-ddns-${ZONE_ID}-${fqdn}-${type}.cache" | tr '/:' '__' } ip_changed() { local current_ip="$1" local cache_file="$2" local cached_ip="" if [[ -f "$cache_file" ]]; then cached_ip="$(cat "$cache_file" 2>/dev/null || true)" fi [[ "$cached_ip" != "$current_ip" ]] } save_ip_cache() { local ip="$1" local cache_file="$2" echo "$ip" > "$cache_file" } # --------- Hetzner Cloud API helpers ---------- api_request() { local method="$1" local url="$2" local data="${3:-}" if [[ -n "$data" ]]; then curl -sS -w "\n%{http_code}" --max-time 30 \ -X "$method" \ -H "Authorization: Bearer $HETZNER_API_TOKEN" \ -H "Content-Type: application/json" \ -d "$data" \ "$url" else curl -sS -w "\n%{http_code}" --max-time 30 \ -X "$method" \ -H "Authorization: Bearer $HETZNER_API_TOKEN" \ "$url" fi } set_rrset() { local zone_id="$1" local fqdn="$2" # full name, e.g. vpn.lenmail.de local type="$3" # A or AAAA local value="$4" local ttl="$5" # API expects "name" as FQDN (as per docs examples), keep as given. # Normalize fqdn="$(echo "$fqdn" | tr '[:upper:]' '[:lower:]')" type="$(echo "$type" | tr '[:lower:]' '[:upper:]')" local request_body request_body='{"name":"'"$fqdn"'","type":"'"$type"'","ttl":'"$ttl"',"records":[{"value":"'"$value"'"}]}' local url="$API_BASE/zones/$zone_id/rrsets" local response http_code body response="$(api_request "POST" "$url" "$request_body")" http_code="$(echo "$response" | tail -n1)" body="$(echo "$response" | sed '$d')" if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then log "RRset set successful: $fqdn $type -> $value (HTTP $http_code)" return 0 fi log "API error setting RRset (HTTP $http_code): $body" return 1 } main() { require_cmd curl load_config log "Getting current public IPs..." local ip4="" ip6="" if [[ "${UPDATE_A}" == "1" ]]; then ip4="$(get_public_ipv4 || true)"; fi if [[ "${UPDATE_AAAA}" == "1" ]]; then ip6="$(get_public_ipv6 || true)"; fi log "Current IPv4: ${ip4:-<none>}" log "Current IPv6: ${ip6:-<none>}" if [[ -z "$ip4" && -z "$ip6" ]]; then error "Failed to get public IPv4/IPv6" fi IFS=',' read -r -a FQDNS <<< "$RECORD_FQDNS" for fqdn in "${FQDNS[@]}"; do fqdn="$(echo "$fqdn" | xargs)" [[ -z "$fqdn" ]] && continue # A if [[ -n "$ip4" && "${UPDATE_A}" == "1" ]]; then local c4 c4="$(cache_file_for "$fqdn" "A")" if ip_changed "$ip4" "$c4"; then log "Updating A $fqdn -> $ip4" set_rrset "$ZONE_ID" "$fqdn" "A" "$ip4" "$TTL" || error "Failed to update A for $fqdn" save_ip_cache "$ip4" "$c4" else log "A $fqdn unchanged, skipping" fi fi # AAAA if [[ -n "$ip6" && "${UPDATE_AAAA}" == "1" ]]; then local c6 c6="$(cache_file_for "$fqdn" "AAAA")" if ip_changed "$ip6" "$c6"; then log "Updating AAAA $fqdn -> $ip6" set_rrset "$ZONE_ID" "$fqdn" "AAAA" "$ip6" "$TTL" || error "Failed to update AAAA for $fqdn" save_ip_cache "$ip6" "$c6" else log "AAAA $fqdn unchanged, skipping" fi fi done log "Done." } main "$@" ``` ----- ## Create Cronjob To run the script automaticly every 5 minutes, add the following file to ``. ```bash */5 * * * * /data/hetzner_ddns/hetzner-cloud-ddns.sh /data/hetzner_ddns/hetzner-cloud-ddns.conf >> /var/log/hetzner-ddns-cron.log 2>&1 ``` ----- ## Example Logs Added new entries ```bash /data/hetzner_ddns/hetzner-cloud-ddns.sh /data/hetzner_ddns/hetzner-cloud-ddns.conf [hetzner-ddns] 2026-02-15 20:25:42 Getting current public IPs... [hetzner-ddns] 2026-02-15 20:25:43 Current IPv4: 217.xx.xx.xx [hetzner-ddns] 2026-02-15 20:25:43 Current IPv6: 2003:a:xx:xxxx:xxxx:8192:a46a:85e9 [hetzner-ddns] 2026-02-15 20:25:43 Updating A vpn -> 217.xx.xx.xx [hetzner-ddns] 2026-02-15 20:25:48 RRset set successful: vpn A -> 217.xx.xx.xx (HTTP 201) [hetzner-ddns] 2026-02-15 20:25:48 Updating AAAA vpn -> 2003:a:xx:xxxx:xxxx:8192:a46a:85e9 [hetzner-ddns] 2026-02-15 20:25:48 RRset set successful: vpn AAAA -> 2003:a:xx:xxxx:xxxx:8192:a46a:85e9 (HTTP 201) [hetzner-ddns] 2026-02-15 20:25:48 Done. ``` Rerun ```bash /data/hetzner_ddns/hetzner-cloud-ddns.sh /data/hetzner_ddns/hetzner-cloud-ddns.conf [hetzner-ddns] 2026-02-15 20:34:11 Getting current public IPs... [hetzner-ddns] 2026-02-15 20:34:22 Current IPv4: 217.xx.xx.xx [hetzner-ddns] 2026-02-15 20:34:22 Current IPv6: 2003:a:xx:xxxx:xxxx:8192:a46a:85e9 [hetzner-ddns] 2026-02-15 20:34:22 A vpn unchanged, skipping [hetzner-ddns] 2026-02-15 20:34:22 AAAA vpn unchanged, skipping [hetzner-ddns] 2026-02-15 20:34:22 Done. ```