Reorganize workspace structure with system-specific cert directories and DNS automation
This commit is contained in:
BIN
ca/ucs-ca-cert.der
Normal file
BIN
ca/ucs-ca-cert.der
Normal file
Binary file not shown.
77
docs/DNS_INTEGRATION.md
Normal file
77
docs/DNS_INTEGRATION.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# DNS Integration Feature
|
||||
|
||||
## Overview
|
||||
The certificate manager now automatically checks if hostnames in certificates are resolvable in DNS and can create missing DNS records on the UCS DNS server.
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Certificate Analysis
|
||||
After signing a certificate, the tool extracts all DNS names from:
|
||||
- Common Name (CN) in the certificate Subject
|
||||
- Subject Alternative Names (SANs)
|
||||
|
||||
### 2. DNS Resolution Check
|
||||
For each hostname found, the tool checks if it resolves using standard DNS lookup.
|
||||
|
||||
### 3. Missing Record Detection
|
||||
If a hostname doesn't resolve, it's flagged as missing.
|
||||
|
||||
### 4. Automatic DNS Record Creation
|
||||
The tool offers to create missing DNS records on the UCS DNS server using:
|
||||
```bash
|
||||
univention-directory-manager dns/host_record create
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
============================================================
|
||||
Step 4: Checking DNS Records
|
||||
============================================================
|
||||
|
||||
Checking 4 hostname(s) from certificate...
|
||||
✓ vscode.egonetix.lan - resolves
|
||||
✓ vscode - resolves
|
||||
✓ srvdocker02.egonetix.lan - resolves
|
||||
✗ newhost.egonetix.lan - NOT found in DNS
|
||||
|
||||
⚠ Found 1 hostname(s) not in DNS:
|
||||
- newhost.egonetix.lan
|
||||
|
||||
Do you want to create missing DNS records on UCS? [Y/n]: y
|
||||
|
||||
Creating DNS records on 10.0.0.21...
|
||||
✓ Created DNS record: newhost.egonetix.lan → 10.0.0.48
|
||||
|
||||
✓ Successfully created 1 DNS record(s)
|
||||
|
||||
Note: DNS changes may take a few seconds to propagate.
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
✅ **Prevents Configuration Errors** - Ensures all certificate hostnames are resolvable
|
||||
✅ **Saves Time** - No need to manually create DNS records
|
||||
✅ **Automatic Workflow** - Integrated into the certificate generation process
|
||||
✅ **Safe** - Always asks for confirmation before creating records
|
||||
✅ **Idempotent** - Detects existing records and skips them
|
||||
|
||||
## Requirements
|
||||
|
||||
- SSH access to UCS DNS server (default: 10.0.0.21)
|
||||
- Root access or UDM permissions on UCS server
|
||||
- Target system must have an IP address for the A record
|
||||
|
||||
## Configuration
|
||||
|
||||
The DNS server is automatically set to the same server as the CA (configured in cert-manager.py):
|
||||
```python
|
||||
config['ca_server'] = '10.0.0.21' # Default UCS server
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only creates A records (IPv4)
|
||||
- Requires the hostname to be part of an existing DNS zone on UCS
|
||||
- Short hostnames (without domain) are skipped
|
||||
- AAAA records (IPv6) not yet supported
|
||||
@@ -4,8 +4,9 @@ Automated certificate generation and signing tools for UCS CA with intelligent s
|
||||
|
||||
## Features
|
||||
|
||||
- 🔍 **Automatic System Detection** - Detects target system type (Proxmox, pfSense, TrueNAS, UCS)
|
||||
- 🔍 **Automatic System Detection** - Detects target system type (Proxmox, Home Assistant, pfSense, TrueNAS, UCS)
|
||||
- 🤖 **Automated Deployment** - Fully automated certificate installation for supported systems
|
||||
- 🌐 **DNS Integration** - Automatically checks and creates DNS records for certificate hostnames
|
||||
- 💾 **Configuration Persistence** - Remembers your settings between runs
|
||||
- 🔐 **Proper Certificate Extensions** - Generates certificates with correct serverAuth extensions
|
||||
- 🎯 **Interactive & Scriptable** - Works both interactively and in automation scripts
|
||||
@@ -46,34 +47,37 @@ The main interactive tool that handles the entire certificate lifecycle with sys
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./cert-manager.py
|
||||
./scripts/cert-manager.py
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
1. Detects target system type automatically
|
||||
2. Prompts for certificate details with smart defaults
|
||||
3. Generates CSR on remote host with proper extensions
|
||||
4. Signs certificate with UCS CA
|
||||
5. Deploys automatically (Proxmox) or copies to target (others)
|
||||
4. Signs certificate with UCS CA (outputs to `certs/<system-type>/` directory)
|
||||
5. **Checks DNS records and offers to create missing ones** 🌐
|
||||
6. Deploys automatically (Proxmox/Home Assistant) or copies to target (others)
|
||||
|
||||
**Features:**
|
||||
- Interactive prompts with default values from previous runs
|
||||
- Automatic system type detection
|
||||
- **Automatic DNS record creation on UCS DNS server**
|
||||
- Intelligent deployment based on system capabilities
|
||||
- Configurable key length (default: 4096 bits)
|
||||
- Remembers last used values for quick reuse
|
||||
- Organized certificate storage by system type
|
||||
|
||||
### 2. generate-csr.sh (Standalone)
|
||||
Generates a certificate signing request on a remote host.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./generate-csr.sh <hostname> <common-name> [country] [state] [locality] [org] [ou] [key-bits]
|
||||
./scripts/generate-csr.sh <hostname> <common-name> [country] [state] [locality] [org] [ou] [key-bits]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
./generate-csr.sh 192.168.1.100 server.example.com DE berlin berlin egonetix it 4096
|
||||
./scripts/generate-csr.sh 192.168.1.100 server.example.com DE berlin berlin egonetix it 4096
|
||||
```
|
||||
|
||||
**Features:**
|
||||
@@ -87,20 +91,22 @@ Signs a certificate request with the UCS CA.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./sign-cert.sh <req-file> <hostname> [days]
|
||||
./scripts/sign-cert.sh <req-file> <hostname> [days]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
./sign-cert.sh server.req server 3650
|
||||
./scripts/sign-cert.sh certs/proxmox/server.csr server 3650
|
||||
```
|
||||
|
||||
**Output:** Certificate is saved to the same directory as the CSR file.
|
||||
|
||||
### 4. detect-system.sh (Utility)
|
||||
Detects the type of system on a remote host.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./detect-system.sh <hostname>
|
||||
./scripts/detect-system.sh <hostname>
|
||||
```
|
||||
|
||||
**Returns:** `proxmox`, `pfsense`, `truenas`, `ucs`, or `unknown`
|
||||
57
docs/STRUCTURE.md
Normal file
57
docs/STRUCTURE.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Folder Structure
|
||||
|
||||
This directory is organized for efficient certificate management:
|
||||
|
||||
## 📁 Structure Overview
|
||||
|
||||
```
|
||||
zertifizierung/
|
||||
├── ca/ # Certificate Authority files
|
||||
│ └── ucs-ca-cert.* # UCS CA certificates (crt, der, pem)
|
||||
│
|
||||
├── certs/ # Generated certificates organized by system
|
||||
│ ├── fritzbox/ # Fritz!Box router certificates
|
||||
│ ├── vscode/ # VS Code server certificates
|
||||
│ ├── proxmox/ # Proxmox host certificates
|
||||
│ ├── homeassistant/ # Home Assistant certificates
|
||||
│ ├── gateway/ # Network gateway certificates
|
||||
│ └── ilo/ # iLO interface certificates
|
||||
│
|
||||
├── scripts/ # Certificate management tools
|
||||
│ ├── cert-manager.py # Main interactive tool
|
||||
│ ├── sign-cert.sh # Sign certificates with UCS CA
|
||||
│ ├── generate-csr*.sh # CSR generation scripts
|
||||
│ ├── deploy-*.sh # Automated deployment scripts
|
||||
│ ├── install-ca-cert.sh # CA certificate installation
|
||||
│ └── detect-system.sh # System type detection
|
||||
│
|
||||
└── docs/ # Documentation
|
||||
├── README.md # Main documentation
|
||||
├── EXAMPLES.md # Usage examples
|
||||
├── DNS_INTEGRATION.md # DNS automation feature
|
||||
└── STRUCTURE.md # This file
|
||||
```
|
||||
|
||||
## 🎯 Usage
|
||||
|
||||
All scripts should be run from the workspace root or scripts directory:
|
||||
|
||||
```bash
|
||||
# Run interactive certificate manager
|
||||
./scripts/cert-manager.py
|
||||
|
||||
# Sign a certificate
|
||||
./scripts/sign-cert.sh certs/fritzbox/fritzbox.csr fritzbox 3650
|
||||
|
||||
# Deploy to Proxmox
|
||||
./scripts/deploy-proxmox.sh certs/proxmox/srv-wmw-host01
|
||||
```
|
||||
|
||||
## 📝 Certificate Files
|
||||
|
||||
Each certificate directory (e.g., `certs/fritzbox/`) typically contains:
|
||||
- `*.key` - Private key
|
||||
- `*.csr` - Certificate signing request
|
||||
- `*.pem` - Signed certificate
|
||||
- `*-cert.pem` - Certificate only
|
||||
- `*-fullchain.pem` - Certificate + CA chain
|
||||
@@ -9,8 +9,14 @@ import sys
|
||||
import json
|
||||
import subprocess
|
||||
import getpass
|
||||
import socket
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Get script directory and workspace root
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
WORKSPACE_ROOT = SCRIPT_DIR.parent
|
||||
|
||||
# Configuration
|
||||
CONFIG_FILE = Path.home() / '.cert-manager-config.json'
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -32,7 +38,8 @@ SYSTEM_TYPES = {
|
||||
'deploy_script': 'deploy-proxmox.sh',
|
||||
'key_location': '/tmp/{short_name}.key',
|
||||
'default_port': '8006',
|
||||
'ssh_user': 'root'
|
||||
'ssh_user': 'root',
|
||||
'cert_dir': 'proxmox'
|
||||
},
|
||||
'homeassistant': {
|
||||
'name': 'Home Assistant',
|
||||
@@ -40,35 +47,40 @@ SYSTEM_TYPES = {
|
||||
'key_location': '/tmp/{short_name}.key',
|
||||
'default_port': '8123',
|
||||
'ssh_user': 'icke',
|
||||
'uses_local_csr': True
|
||||
'uses_local_csr': True,
|
||||
'cert_dir': 'homeassistant'
|
||||
},
|
||||
'pfsense': {
|
||||
'name': 'pfSense',
|
||||
'deploy_script': None, # Manual deployment via web interface
|
||||
'key_location': '/tmp/{short_name}.key',
|
||||
'default_port': '443',
|
||||
'ssh_user': 'root'
|
||||
'ssh_user': 'root',
|
||||
'cert_dir': 'pfsense'
|
||||
},
|
||||
'truenas': {
|
||||
'name': 'TrueNAS',
|
||||
'deploy_script': None, # Manual deployment via web interface
|
||||
'key_location': '/tmp/{short_name}.key',
|
||||
'default_port': '443',
|
||||
'ssh_user': 'root'
|
||||
'ssh_user': 'root',
|
||||
'cert_dir': 'truenas'
|
||||
},
|
||||
'ucs': {
|
||||
'name': 'Univention Corporate Server',
|
||||
'deploy_script': None,
|
||||
'key_location': '/tmp/{short_name}.key',
|
||||
'default_port': '443',
|
||||
'ssh_user': 'root'
|
||||
'ssh_user': 'root',
|
||||
'cert_dir': 'ucs'
|
||||
},
|
||||
'unknown': {
|
||||
'name': 'Unknown System',
|
||||
'deploy_script': None,
|
||||
'key_location': '/tmp/{short_name}.key',
|
||||
'default_port': '443',
|
||||
'ssh_user': 'root'
|
||||
'ssh_user': 'root',
|
||||
'cert_dir': 'other'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +170,145 @@ def detect_system_type(target_host, script_dir, ssh_user=None, ssh_password=None
|
||||
print(f"Warning: Could not detect system type: {e}")
|
||||
return 'unknown', None
|
||||
|
||||
def check_dns_resolution(hostname):
|
||||
"""Check if a hostname resolves in DNS"""
|
||||
try:
|
||||
socket.gethostbyname(hostname)
|
||||
return True
|
||||
except socket.gaierror:
|
||||
return False
|
||||
|
||||
def extract_hostnames_from_cert(cert_file):
|
||||
"""Extract all DNS names from a certificate"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['openssl', 'x509', '-in', cert_file, '-text', '-noout'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
hostnames = []
|
||||
|
||||
# Extract CN from Subject (not from Issuer!)
|
||||
subject_match = re.search(r'Subject:.*?CN\s*=\s*([^,\n]+)', result.stdout, re.DOTALL)
|
||||
if subject_match:
|
||||
cn = subject_match.group(1).strip()
|
||||
# Only add if it looks like a hostname (contains letters and possibly dots/dashes)
|
||||
if re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$', cn):
|
||||
hostnames.append(cn)
|
||||
|
||||
# Extract SANs
|
||||
san_match = re.search(r'X509v3 Subject Alternative Name:.*?DNS:([^\n]+)', result.stdout, re.DOTALL)
|
||||
if san_match:
|
||||
san_text = san_match.group(1)
|
||||
# Extract all DNS entries
|
||||
dns_entries = re.findall(r'DNS:([^,\s]+)', san_text)
|
||||
hostnames.extend(dns_entries)
|
||||
|
||||
# Remove duplicates and filter out IP addresses
|
||||
unique_hostnames = []
|
||||
for h in hostnames:
|
||||
h = h.strip()
|
||||
# Skip if it looks like an IP address or contains special chars from CA names
|
||||
if not re.match(r'^\d+\.\d+\.\d+\.\d+$', h) and '(' not in h and ')' not in h:
|
||||
if h not in unique_hostnames and len(h) > 0:
|
||||
unique_hostnames.append(h)
|
||||
|
||||
return unique_hostnames
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not extract hostnames from certificate: {e}")
|
||||
return []
|
||||
|
||||
def create_dns_record(hostname, target_ip, dns_server='10.0.0.21'):
|
||||
"""Create a DNS record on UCS server"""
|
||||
try:
|
||||
# Split hostname into name and domain
|
||||
parts = hostname.split('.', 1)
|
||||
if len(parts) == 1:
|
||||
# Short hostname without domain - skip it
|
||||
return False
|
||||
|
||||
name = parts[0]
|
||||
domain = parts[1]
|
||||
|
||||
# Construct the zone DN
|
||||
zone_parts = domain.split('.')
|
||||
zone_dn = f"zoneName={domain},cn=dns,dc={',dc='.join(zone_parts)}"
|
||||
|
||||
# Create DNS record via SSH to UCS server
|
||||
cmd = [
|
||||
'ssh', f'root@{dns_server}',
|
||||
'univention-directory-manager', 'dns/host_record', 'create',
|
||||
'--superordinate', zone_dn,
|
||||
'--set', f'name={name}',
|
||||
'--set', f'a={target_ip}'
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f" ✓ Created DNS record: {hostname} → {target_ip}")
|
||||
return True
|
||||
else:
|
||||
if 'already exists' in result.stderr.lower():
|
||||
print(f" ℹ DNS record already exists: {hostname}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ Failed to create DNS record for {hostname}: {result.stderr.strip()}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ✗ Error creating DNS record for {hostname}: {e}")
|
||||
return False
|
||||
|
||||
def check_and_create_dns_records(cert_file, target_ip, dns_server='10.0.0.21'):
|
||||
"""Check DNS records for certificate hostnames and offer to create missing ones"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 4: Checking DNS Records")
|
||||
print("=" * 60)
|
||||
|
||||
hostnames = extract_hostnames_from_cert(cert_file)
|
||||
|
||||
if not hostnames:
|
||||
print("No hostnames found in certificate to check.")
|
||||
return
|
||||
|
||||
print(f"\nChecking {len(hostnames)} hostname(s) from certificate...")
|
||||
|
||||
missing_hostnames = []
|
||||
for hostname in hostnames:
|
||||
if check_dns_resolution(hostname):
|
||||
print(f" ✓ {hostname} - resolves")
|
||||
else:
|
||||
print(f" ✗ {hostname} - NOT found in DNS")
|
||||
missing_hostnames.append(hostname)
|
||||
|
||||
if not missing_hostnames:
|
||||
print("\n✓ All hostnames are resolvable in DNS!")
|
||||
return
|
||||
|
||||
print(f"\n⚠ Found {len(missing_hostnames)} hostname(s) not in DNS:")
|
||||
for hostname in missing_hostnames:
|
||||
print(f" - {hostname}")
|
||||
|
||||
if yes_no_prompt("\nDo you want to create missing DNS records on UCS?", True):
|
||||
print(f"\nCreating DNS records on {dns_server}...")
|
||||
|
||||
success_count = 0
|
||||
for hostname in missing_hostnames:
|
||||
if create_dns_record(hostname, target_ip, dns_server):
|
||||
success_count += 1
|
||||
|
||||
if success_count > 0:
|
||||
print(f"\n✓ Successfully created {success_count} DNS record(s)")
|
||||
print("\nNote: DNS changes may take a few seconds to propagate.")
|
||||
else:
|
||||
print("\n⚠ No DNS records were created")
|
||||
else:
|
||||
print("\nSkipping DNS record creation.")
|
||||
print("You may need to create these records manually for the certificate to work properly.")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Interactive Certificate Manager")
|
||||
@@ -271,6 +422,15 @@ def main():
|
||||
print("Cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
# Create output directory for this system type
|
||||
cert_dir = WORKSPACE_ROOT / 'certs' / system_info.get('cert_dir', 'other')
|
||||
cert_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Change to cert output directory
|
||||
original_dir = Path.cwd()
|
||||
os.chdir(cert_dir)
|
||||
print(f"\nOutput directory: {cert_dir}")
|
||||
|
||||
# Save config for next run
|
||||
config['last_target_host'] = target_host
|
||||
config['last_common_name'] = common_name
|
||||
@@ -280,6 +440,9 @@ def main():
|
||||
print("Step 1: Generating CSR")
|
||||
print("=" * 60)
|
||||
|
||||
# Get script directory
|
||||
script_dir = SCRIPT_DIR
|
||||
|
||||
# Check if system requires local CSR generation
|
||||
if system_info.get('uses_local_csr', False):
|
||||
# Generate CSR locally for Home Assistant and similar systems
|
||||
@@ -359,12 +522,16 @@ def main():
|
||||
print(f"\nError: sign-cert.sh not found in {script_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 3: Deploying certificate to target host")
|
||||
print("=" * 60)
|
||||
|
||||
# Certificate file path
|
||||
cert_file = f"{short_name}-cert.pem"
|
||||
|
||||
# Check and create DNS records if needed
|
||||
check_and_create_dns_records(cert_file, target_host, config['ca_server'])
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 5: Deploying certificate to target host")
|
||||
print("=" * 60)
|
||||
|
||||
# Use system-specific deployment if available
|
||||
if system_info['deploy_script']:
|
||||
if yes_no_prompt(f"Deploy certificate to {system_info['name']} automatically?", True):
|
||||
@@ -34,7 +34,9 @@ fi
|
||||
|
||||
# Get absolute path of req file
|
||||
REQ_FILE=$(realpath "$REQ_FILE")
|
||||
OUTPUT_FILE="${HOSTNAME}-cert.pem"
|
||||
# Output to same directory as input CSR
|
||||
REQ_DIR=$(dirname "$REQ_FILE")
|
||||
OUTPUT_FILE="${REQ_DIR}/${HOSTNAME}-cert.pem"
|
||||
|
||||
echo "=========================================="
|
||||
echo "UCS Certificate Signing Script"
|
||||
Reference in New Issue
Block a user