Security: Remove hardcoded credentials, enforcing env vars

This commit is contained in:
Gemini Agent
2025-12-04 05:24:36 +00:00
parent 76dffc4c0d
commit 27b7187f3a
4 changed files with 97 additions and 96 deletions

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# .env Example Configuration
# Copy this file to .env and adjust the values
# --- Required ---
NETBOX_URL=http://netbox.example.com
NETBOX_TOKEN=0123456789abcdef0123456789abcdef01234567
# --- Optional (Defaults shown) ---
# ZONE_NAME=klenzel.net
# REVERSE_ZONE_NAME=172.in-addr.arpa
# REFRESH_INTERVAL=600
# FALLBACK_NS_HOSTNAME=fks-01-cl-cdns
# FALLBACK_NS_IP=172.25.16.152

View File

@@ -1,17 +1,17 @@
FROM python:3.11-alpine FROM python:3.11-alpine
# Standard-Environment-Variablen (können beim Start überschrieben werden) # Defaults für nicht-sensitive Konfiguration
ENV NETBOX_URL="http://172.30.242.99" \ ENV REFRESH_INTERVAL=600 \
NETBOX_TOKEN="0b740520aeef964cfd2ca34b82a472a271d51649" \
ZONE_NAME="klenzel.net" \ ZONE_NAME="klenzel.net" \
REVERSE_ZONE_NAME="172.in-addr.arpa" \ REVERSE_ZONE_NAME="172.in-addr.arpa" \
REFRESH_INTERVAL=600 \
OUTPUT_FILE_FWD="/zones/db.klenzel.net" \ OUTPUT_FILE_FWD="/zones/db.klenzel.net" \
OUTPUT_FILE_REV="/zones/db.reverse.arpa" \ OUTPUT_FILE_REV="/zones/db.reverse.arpa" \
DEFAULT_TTL=3600 \ DEFAULT_TTL=3600 \
FALLBACK_NS_HOSTNAME="fks-01-cl-cdns" \ FALLBACK_NS_HOSTNAME="fks-01-cl-cdns" \
FALLBACK_NS_IP="172.25.16.152" FALLBACK_NS_IP="172.25.16.152"
# Hinweis: NETBOX_URL und NETBOX_TOKEN müssen zur Laufzeit übergeben werden!
RUN pip install requests RUN pip install requests
COPY sync.py /sync.py COPY sync.py /sync.py
@@ -19,4 +19,4 @@ COPY sync.py /sync.py
# Logs ungepuffert ausgeben # Logs ungepuffert ausgeben
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
CMD ["python", "/sync.py"] CMD ["python", "/sync.py"]

124
README.md
View File

@@ -1,75 +1,73 @@
# CoreDNS NetBox Sync # CoreDNS NetBox Sync
Dieser Container synchronisiert DNS-Records aus [NetBox](https://github.com/netbox-community/netbox) in lokale Zonefiles, die von [CoreDNS](https://coredns.io/) (oder BIND) genutzt werden können. This container automates the generation of DNS zone files for CoreDNS (or BIND) by synchronizing data from [NetBox](https://github.com/netbox-community/netbox). It fetches IPAM data (active IPs with DNS names) and DNS Plugin records.
## Features ## Features
- **Automatische Synchronisation:** Holt alle 10 Minuten (konfigurierbar) Daten aus NetBox. * **Automated Synchronization:** Periodically fetches data from NetBox (Default: 10 minutes).
- **Forward & Reverse Zones:** Erstellt A/AAAA und PTR Records. * **Fail-Safe Operation:** If NetBox is unreachable or returns errors, the existing zone files are preserved to prevent `NXDOMAIN` issues.
- **Ausfallsicherheit:** Wenn NetBox nicht erreichbar ist oder die API Fehler wirft, werden **keine** leeren Dateien geschrieben. Die alten Zonefiles bleiben erhalten, um `NXDOMAIN` Antworten zu verhindern. * **Dual Zone Support:** Generates both Forward and Reverse (PTR) zones.
- **Fallback NS:** Konfigurierbarer Fallback-Nameserver, falls in NetBox keine NS-Records für die Zone definiert sind. * **Intelligent Fallback:** Automatically configures a fallback Nameserver if no NS records are defined in NetBox.
## Installation & Nutzung ## Configuration
### 1. Image bauen Configuration is handled entirely via environment variables.
```bash ### Required Variables
docker build -t local/dns-sync .
```
### 2. Container starten | Variable | Description |
| :--- | :--- |
| `NETBOX_URL` | The full URL to the NetBox instance (e.g., `http://netbox.local`). |
| `NETBOX_TOKEN` | The API Token for authentication (Read-Only permissions are sufficient). |
Da die Standardwerte im Dockerfile hinterlegt sind, reicht dein bisheriger Befehl völlig aus, solange sich an der Konfiguration nichts geändert hat: ### Optional Variables
```bash | Variable | Default | Description |
docker run -d \
--name klzDNS-worker \
--restart unless-stopped \
--net=container:klzDNS-coredns \
-v klzDNS-data:/zones \
local/dns-sync
```
### 3. Konfiguration anpassen (Optional)
Möchtest du Werte ändern (z.B. URL, Token oder Interval), kannst du diese als Umgebungsvariablen (`-e`) übergeben:
```bash
docker run -d \
--name klzDNS-worker \
--restart unless-stopped \
--net=container:klzDNS-coredns \
-v klzDNS-data:/zones \
-e NETBOX_URL="http://deine-netbox-url" \
-e NETBOX_TOKEN="dein-neuer-token" \
-e REFRESH_INTERVAL=300 \
local/dns-sync
```
Alternativ kannst du eine `.env` Datei erstellen:
```ini
# .env Datei
NETBOX_URL=http://192.168.1.50
REFRESH_INTERVAL=60
```
Und diese einbinden:
```bash
docker run -d ... --env-file .env local/dns-sync
```
## Verfügbare Variablen
| Variable | Standardwert | Beschreibung |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| `NETBOX_URL` | `http://172.30.242.99` | URL zur NetBox Instanz | | `REFRESH_INTERVAL` | `600` | Synchronization interval in seconds. |
| `NETBOX_TOKEN` | `0b74...` | API Token (Read-Only reicht) | | `ZONE_NAME` | `klenzel.net` | The DNS zone name to manage. |
| `ZONE_NAME` | `klenzel.net` | Die zu verwaltende DNS-Zone | | `REVERSE_ZONE_NAME` | `172.in-addr.arpa` | The reverse lookup zone name. |
| `REVERSE_ZONE_NAME` | `172.in-addr.arpa` | Reverse Lookup Zone | | `OUTPUT_FILE_FWD` | `/zones/db.klenzel.net` | Path inside the container for the forward zone file. |
| `REFRESH_INTERVAL` | `600` | Sync-Intervall in Sekunden | | `OUTPUT_FILE_REV` | `/zones/db.reverse.arpa` | Path inside the container for the reverse zone file. |
| `OUTPUT_FILE_FWD` | `/zones/db.klenzel.net` | Pfad zur Forward Zone im Container | | `FALLBACK_NS_HOSTNAME`| `fks-01-cl-cdns` | Hostname used for NS record if none exist in NetBox. |
| `OUTPUT_FILE_REV` | `/zones/db.reverse.arpa` | Pfad zur Reverse Zone im Container | | `FALLBACK_NS_IP` | `172.25.16.152` | IP address for the fallback NS glue record. |
| `FALLBACK_NS_HOSTNAME`| `fks-01-cl-cdns` | Hostname des NS, falls keiner in NetBox definiert ist |
| `FALLBACK_NS_IP` | `172.25.16.152` | IP des Fallback NS (für Glue Record) | ## Usage
### Docker
1. **Build the image:**
```bash
docker build -t local/dns-sync .
```
2. **Run with environment variables:**
```bash
docker run -d \
--name klzDNS-worker \
--restart unless-stopped \
--net=container:klzDNS-coredns \
-v klzDNS-data:/zones \
-e NETBOX_URL="http://172.30.242.99" \
-e NETBOX_TOKEN="your-secret-token" \
local/dns-sync
```
### Using a `.env` file
1. Create a `.env` file based on the example:
```bash
cp .env.example .env
# Edit .env and add your credentials
```
2. Run the container referencing the file:
```bash
docker run -d \
--name klzDNS-worker \
--restart unless-stopped \
--net=container:klzDNS-coredns \
-v klzDNS-data:/zones \
--env-file .env \
local/dns-sync
```

46
sync.py
View File

@@ -3,17 +3,24 @@ import os
import time import time
import sys import sys
# --- KONFIGURATION (Via Environment Variables) --- def log(msg):
# Falls keine ENV-Vars gesetzt sind, werden die alten Hardcoded-Werte als Fallback genutzt. """Ungepufferte Ausgabe für Docker Logs"""
NETBOX_URL = os.getenv("NETBOX_URL", "http://172.30.242.99") print(msg, flush=True)
TOKEN = os.getenv("NETBOX_TOKEN", "0b740520aeef964cfd2ca34b82a472a271d51649")
# --- KONFIGURATION ---
# Pflichtfelder - Abbruch wenn nicht gesetzt
NETBOX_URL = os.getenv("NETBOX_URL")
TOKEN = os.getenv("NETBOX_TOKEN")
if not NETBOX_URL or not TOKEN:
log("FATAL ERROR: Environment variables NETBOX_URL and NETBOX_TOKEN are required.")
log("Please provide them via -e flag or .env file.")
sys.exit(1)
# Optionale Konfiguration mit Defaults
ZONE_NAME = os.getenv("ZONE_NAME", "klenzel.net") ZONE_NAME = os.getenv("ZONE_NAME", "klenzel.net")
REVERSE_ZONE_NAME = os.getenv("REVERSE_ZONE_NAME", "172.in-addr.arpa") REVERSE_ZONE_NAME = os.getenv("REVERSE_ZONE_NAME", "172.in-addr.arpa")
# Interval in Sekunden
REFRESH_INTERVAL = int(os.getenv("REFRESH_INTERVAL", "600")) REFRESH_INTERVAL = int(os.getenv("REFRESH_INTERVAL", "600"))
OUTPUT_FILE_FWD = os.getenv("OUTPUT_FILE_FWD", "/zones/db.klenzel.net") OUTPUT_FILE_FWD = os.getenv("OUTPUT_FILE_FWD", "/zones/db.klenzel.net")
OUTPUT_FILE_REV = os.getenv("OUTPUT_FILE_REV", "/zones/db.reverse.arpa") OUTPUT_FILE_REV = os.getenv("OUTPUT_FILE_REV", "/zones/db.reverse.arpa")
DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600") DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600")
@@ -21,18 +28,13 @@ DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600")
# Fallback Konfiguration (wenn KEIN NS in NetBox gefunden wird) # Fallback Konfiguration (wenn KEIN NS in NetBox gefunden wird)
FALLBACK_NS_HOSTNAME = os.getenv("FALLBACK_NS_HOSTNAME", "fks-01-cl-cdns") FALLBACK_NS_HOSTNAME = os.getenv("FALLBACK_NS_HOSTNAME", "fks-01-cl-cdns")
FALLBACK_NS_IP = os.getenv("FALLBACK_NS_IP", "172.25.16.152") FALLBACK_NS_IP = os.getenv("FALLBACK_NS_IP", "172.25.16.152")
# ---------------------
HEADERS = {'Authorization': f'Token {TOKEN}', 'Accept': 'application/json'} HEADERS = {'Authorization': f'Token {TOKEN}', 'Accept': 'application/json'}
def log(msg):
"""Ungepufferte Ausgabe für Docker Logs"""
print(msg, flush=True)
def fetch_ipam_data(): def fetch_ipam_data():
""" """
Holt alle aktiven IPs mit DNS-Namen aus NetBox. Holt alle aktiven IPs mit DNS-Namen aus NetBox.
Wirft eine Exception, wenn NetBox nicht erreichbar ist (kein try/except hier!). Wirft eine Exception, wenn NetBox nicht erreichbar ist.
""" """
url = f"{NETBOX_URL}/api/ipam/ip-addresses/?status=active&dns_name__n=&limit=0" url = f"{NETBOX_URL}/api/ipam/ip-addresses/?status=active&dns_name__n=&limit=0"
log(f"Abruf IPAM: {url}") log(f"Abruf IPAM: {url}")
@@ -114,7 +116,6 @@ def get_ns_config(plugin_records, all_records):
def generate_zone_file_fwd(ipam_data, plugin_records): def generate_zone_file_fwd(ipam_data, plugin_records):
"""Erstellt die Forward-Zone""" """Erstellt die Forward-Zone"""
# Daten aufbereiten
ipam_records = [] ipam_records = []
for ip in ipam_data: for ip in ipam_data:
dns_name = ip.get('dns_name', '') dns_name = ip.get('dns_name', '')
@@ -133,7 +134,6 @@ def generate_zone_file_fwd(ipam_data, plugin_records):
all_records = plugin_records + ipam_records all_records = plugin_records + ipam_records
# Header-Logik
primary_ns, extra_headers = get_ns_config(plugin_records, all_records) primary_ns, extra_headers = get_ns_config(plugin_records, all_records)
serial = int(time.time()) serial = int(time.time())
@@ -152,7 +152,6 @@ def generate_zone_file_fwd(ipam_data, plugin_records):
header.extend(extra_headers) header.extend(extra_headers)
header.append("") header.append("")
# Schreiben
with open(OUTPUT_FILE_FWD, "w") as f: with open(OUTPUT_FILE_FWD, "w") as f:
f.write("\n".join(header)) f.write("\n".join(header))
f.write("\n; --- Records ---\n") f.write("\n; --- Records ---\n")
@@ -207,11 +206,10 @@ def generate_zone_file_rev(ipam_data, plugin_records):
log(f"SUCCESS: Reverse Zone geschrieben ({len(ptr_records)} Records).") log(f"SUCCESS: Reverse Zone geschrieben ({len(ptr_records)} Records).")
if __name__ == "__main__": if __name__ == "__main__":
# Kurzer Check ob Zielordner existiert
if not os.path.exists(os.path.dirname(OUTPUT_FILE_FWD)): if not os.path.exists(os.path.dirname(OUTPUT_FILE_FWD)):
log(f"FATAL: Verzeichnis {os.path.dirname(OUTPUT_FILE_FWD)} fehlt!") log(f"FATAL: Verzeichnis {os.path.dirname(OUTPUT_FILE_FWD)} fehlt!")
time.sleep(30) time.sleep(5)
exit(1) sys.exit(1)
log(f"--- CoreDNS Sync startet ---") log(f"--- CoreDNS Sync startet ---")
log(f"NetBox URL: {NETBOX_URL}") log(f"NetBox URL: {NETBOX_URL}")
@@ -221,22 +219,14 @@ if __name__ == "__main__":
log(f"\n--- Sync Start: {time.ctime()} ---") log(f"\n--- Sync Start: {time.ctime()} ---")
try: try:
# Schritt 1: Daten holen (wirft Exception bei Fehler)
# Wir holen BEIDE Datensätze, bevor wir irgendetwas schreiben.
# Wenn einer fehlschlägt, bricht der ganze Block ab.
ipam_data = fetch_ipam_data() ipam_data = fetch_ipam_data()
plugin_records = fetch_plugin_records() plugin_records = fetch_plugin_records()
# Schritt 2: Zonen schreiben
# Wird nur erreicht, wenn fetch_ipam_data UND fetch_plugin_records erfolgreich waren
generate_zone_file_fwd(ipam_data, plugin_records) generate_zone_file_fwd(ipam_data, plugin_records)
generate_zone_file_rev(ipam_data, plugin_records) generate_zone_file_rev(ipam_data, plugin_records)
except Exception as e: except Exception as e:
# Sicherheits-Logik: Bei JEDEM Fehler (Netzwerk, HTTP 500, Parse-Error)
# fangen wir hier ab und schreiben KEINE Dateien.
# Die alten Dateien bleiben erhalten -> CoreDNS liefert weiter die alten Daten (kein NXDOMAIN).
log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}") log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}")
log(f"Schlafe {REFRESH_INTERVAL} Sekunden...") log(f"Schlafe {REFRESH_INTERVAL} Sekunden...")
time.sleep(REFRESH_INTERVAL) time.sleep(REFRESH_INTERVAL)