Commit 8f5f4c13c74e

Vincent Demeester <vincent@sbr.pm>
2026-07-10 14:33:04
feat(dns): add HTTPS record support to Gandi DNS
Added HTTPS (RFC 9460 / TYPE65) record management for domains that need it. Records are defined separately since nix-community/dns.nix doesn't support HTTPS records yet. Includes create, update, skip-if-unchanged logic and dry-run support.
1 parent 7b8a030
Changed files (1)
tools/update-gandi-dns.sh
@@ -24,6 +24,14 @@ DOMAIN_ZONE_FILES["demeester.fr"]="./systems/common/services/dns/demeester.fr.ni
 ALL_DOMAINS=("sbr.pm" "demeester.fr")
 SELECTED_DOMAINS=()
 
+# HTTPS DNS records (RFC 9460) per domain
+# These can't be expressed in nix-community/dns.nix yet, so we manage them here.
+# Format: "name:priority target params" (name is @ for apex)
+# See: https://docs.gandi.net/en/domain_names/faq/record_types/https_record.html
+declare -A DOMAIN_HTTPS_RECORDS
+# Note: Gandi requires an explicit FQDN target (not ".") for HTTPS records.
+DOMAIN_HTTPS_RECORDS["demeester.fr"]='@:1 demeester.fr. alpn="h3,h2"|vincent:1 vincent.demeester.fr. alpn="h3,h2"|www:1 www.demeester.fr. alpn="h3,h2"|*:1 demeester.fr. alpn="h3,h2"'
+
 # Parse arguments
 for arg in "$@"; do
     case $arg in
@@ -106,6 +114,7 @@ update_domain() {
                   grep -v "^;" || true)
 
     # Extract MX records
+    # shellcheck disable=SC2034
     MX_RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?MX\s+" "$TEMP_ZONE" | \
                  grep -v "^;" || true)
 
@@ -151,7 +160,7 @@ update_domain() {
             if [[ "$FULL_NAME" == "$DOMAIN." ]]; then
                 NAME="@"
             else
-                NAME="${FULL_NAME%.$DOMAIN.}"
+                NAME="${FULL_NAME%."$DOMAIN".}"
             fi
 
             DESIRED_NAMES["$NAME"]=1
@@ -217,7 +226,7 @@ update_domain() {
                 VALUE=$(echo "$line" | awk '{print $4}')
             fi
 
-            NAME="${FULL_NAME%.$DOMAIN.}"
+            NAME="${FULL_NAME%."$DOMAIN".}"
 
             echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN CNAME $VALUE (TTL: $TTL)"
 
@@ -276,7 +285,7 @@ update_domain() {
             if [[ "$FULL_NAME" == "$DOMAIN." ]]; then
                 NAME="@"
             else
-                NAME="${FULL_NAME%.$DOMAIN.}"
+                NAME="${FULL_NAME%."$DOMAIN".}"
             fi
 
             # Extract TXT value (everything after TXT, keeping quotes)
@@ -353,6 +362,64 @@ update_domain() {
         unset TXT_BY_NAME
     fi
 
+    # Process HTTPS records (RFC 9460 / TYPE65)
+    if [[ -n "${DOMAIN_HTTPS_RECORDS[$DOMAIN]+_}" ]]; then
+        echo -e "${BOLD}${BLUE}Processing HTTPS (TYPE65) records...${RESET}"
+        echo
+
+        IFS='|' read -ra HTTPS_ENTRIES <<< "${DOMAIN_HTTPS_RECORDS[$DOMAIN]}"
+        for entry in "${HTTPS_ENTRIES[@]}"; do
+            NAME="${entry%%:*}"
+            VALUE="${entry#*:}"
+
+            echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN HTTPS $VALUE"
+
+            if [[ "$DRY_RUN" == "true" ]]; then
+                echo -e "  ${CYAN}[DRY RUN] Would update/create HTTPS record${RESET}"
+                UPDATED=$((UPDATED + 1))
+            else
+                CURRENT_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
+                  --arg name "$NAME" \
+                  --arg type "HTTPS" \
+                  '.[] | select(.rrset_name == $name and .rrset_type == $type) | .rrset_values[0]' \
+                  2>/dev/null || echo "")
+
+                if [[ "$CURRENT_VALUE" == "$VALUE" ]]; then
+                    echo -e "  ${GREEN}โœ“ Record unchanged, skipping${RESET}"
+                    SKIPPED=$((SKIPPED + 1))
+                else
+                    if [[ -z "$CURRENT_VALUE" ]]; then
+                        echo -e "  ${YELLOW}Creating new HTTPS record...${RESET}"
+                    else
+                        echo -e "  ${YELLOW}Updating HTTPS record (was: $CURRENT_VALUE)...${RESET}"
+                    fi
+
+                    PAYLOAD=$(jq -n --arg v "$VALUE" '{rrset_values: [$v], rrset_ttl: 10800}')
+                    RESPONSE=$(curl -s -w "\n%{http_code}" \
+                      -X PUT \
+                      -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
+                      -H "Content-Type: application/json" \
+                      -d "$PAYLOAD" \
+                      "$API_URL/$NAME/HTTPS")
+
+                    HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
+                    BODY=$(echo "$RESPONSE" | sed '$d')
+
+                    if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
+                        echo -e "  ${GREEN}โœ“ HTTPS record updated successfully${RESET}"
+                        UPDATED=$((UPDATED + 1))
+                    else
+                        echo -e "  ${RED}โœ— Failed to update HTTPS record (HTTP $HTTP_CODE)${RESET}"
+                        echo -e "  ${RED}Response: $BODY${RESET}"
+                        FAILED=$((FAILED + 1))
+                    fi
+                fi
+            fi
+
+            echo
+        done
+    fi
+
     # Remove stale A records from Gandi that are no longer in Nix config
     echo -e "${BOLD}${BLUE}Checking for stale A records to remove...${RESET}"
     echo