Add programmatic DNS with Transip API

This commit is contained in:
Ruben Hensen
2026-03-14 21:28:54 +01:00
parent c0f1489177
commit bd9053eeec
6 changed files with 233 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: dns-sync
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
serviceAccountName: dns-sync
restartPolicy: Never
containers:
- name: sync
image: python:3.12-alpine
command:
- sh
- -c
- |
pip install --quiet PyJWT cryptography requests pyyaml &&
python /config/sync.py
env:
- name: TRANSIP_ACCOUNT_NAME
valueFrom:
secretKeyRef:
name: transip-credentials
key: account_name
- name: TRANSIP_PRIVATE_KEY_PATH
value: /secrets/private_key
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
- name: script
mountPath: /config/sync.py
subPath: sync.py
readOnly: true
- name: domains
mountPath: /config/domains
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: secrets
secret:
secretName: transip-credentials
items:
- key: private_key
path: private_key
- name: script
configMap:
name: dns-sync-script
- name: domains
configMap:
name: dns-domains
+17
View File
@@ -0,0 +1,17 @@
records:
- name: "@"
expire: 86400
type: A
content: "62.41.87.114"
- name: mail
expire: 86400
type: A
content: "46.224.26.65"
- name: "phocaslustrum"
expire: 86400
type: CNAME
content: "@"
- name: "ynab"
expire: 86400
type: CNAME
content: "@"
+21
View File
@@ -0,0 +1,21 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: transip-credentials
spec:
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
refreshInterval: 15m
target:
name: transip-credentials
creationPolicy: Owner
data:
- secretKey: private_key
remoteRef:
key: kv/transip
property: private_key
- secretKey: account_name
remoteRef:
key: kv/transip
property: account_name
+17
View File
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dns
resources:
- external-secret.yaml
- rbac.yaml
- cronjob.yaml
configMapGenerator:
- name: dns-sync-script
files:
- sync.py
- name: dns-domains
files:
- domains/rubenhensen.nl.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: dns-sync
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Sync DNS records from YAML files to TransIP via their REST API."""
import base64
import json
import os
import sys
import time
import uuid
from glob import glob
from pathlib import Path
import jwt
import requests
import yaml
TRANSIP_API = "https://api.transip.nl/v6"
DOMAINS_DIR = "/config/domains"
def get_access_token(account_name: str, private_key: str) -> str:
"""Authenticate with TransIP API and return a bearer token."""
now = int(time.time())
payload = {
"iss": account_name,
"sub": account_name,
"aud": "api.transip.nl",
"jti": str(uuid.uuid4()),
"iat": now,
"nbf": now,
"exp": now + 300,
"global_key": True,
}
token = jwt.encode(payload, private_key, algorithm="RS512")
resp = requests.post(
f"{TRANSIP_API}/auth",
json={"login": account_name, "nonce": payload["jti"], "global_key": True},
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["token"]
def sync_domain(domain: str, records: list, token: str) -> None:
"""Replace all DNS entries for a domain."""
entries = []
for r in records:
entries.append({
"name": r["name"],
"expire": r["expire"],
"type": r["type"],
"content": r["content"],
})
resp = requests.put(
f"{TRANSIP_API}/domains/{domain}/dns",
json={"dnsEntries": entries},
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
},
timeout=30,
)
resp.raise_for_status()
print(f"Synced {len(entries)} records for {domain}")
def main():
account_name = os.environ.get("TRANSIP_ACCOUNT_NAME")
private_key_path = os.environ.get("TRANSIP_PRIVATE_KEY_PATH", "/secrets/private_key")
if not account_name:
print("ERROR: TRANSIP_ACCOUNT_NAME not set")
sys.exit(1)
private_key = Path(private_key_path).read_text().strip()
print("Authenticating with TransIP API...")
token = get_access_token(account_name, private_key)
domain_files = sorted(glob(f"{DOMAINS_DIR}/*.yaml"))
if not domain_files:
print("No domain files found")
sys.exit(0)
errors = 0
for filepath in domain_files:
domain = Path(filepath).stem
print(f"Processing {domain}...")
try:
with open(filepath) as f:
data = yaml.safe_load(f)
sync_domain(domain, data["records"], token)
except Exception as e:
print(f"ERROR syncing {domain}: {e}")
errors += 1
if errors:
print(f"Completed with {errors} error(s)")
sys.exit(1)
print("All domains synced successfully")
if __name__ == "__main__":
main()