Building Secure Local SSL Infrastructure with DNS
Creating a secure internal SSL/TLS infrastructure is essential for protecting communication between services in local networks, development environments, and private clouds. This guide provides a complete implementation for building a production-ready local SSL infrastructure integrated with DNS for automatic certificate validation and distribution.
Architecture Overview
A properly designed local SSL infrastructure consists of several key components working together:
graph TB
subgraph "Certificate Authority"
CA[Root CA]
ICA[Intermediate CA]
CA --> ICA
end
subgraph "DNS Infrastructure"
DNS[Internal DNS Server]
ACME[ACME DNS Challenge]
DNS --> ACME
end
subgraph "Certificate Management"
CM[Cert Manager]
VAULT[HashiCorp Vault]
CM --> VAULT
end
subgraph "Services"
WEB[Web Services]
API[API Services]
DB[Database Services]
end
ICA --> CM
CM --> WEB
CM --> API
CM --> DB
ACME --> CM
subgraph "Clients"
BROWSER[Browsers]
APP[Applications]
CLI[CLI Tools]
end
WEB --> BROWSER
API --> APP
DB --> CLI
Setting Up the Certificate Authority
Step 1: Create Root Certificate Authority
First, establish a secure root CA that will be the trust anchor for your infrastructure:
#!/bin/bash
# create-root-ca.sh
# Configuration
CA_DIR="/etc/ssl/ca"
ROOT_KEY="$CA_DIR/private/root-ca.key"
ROOT_CERT="$CA_DIR/certs/root-ca.crt"
ROOT_CONFIG="$CA_DIR/root-ca.conf"
# Create directory structure
mkdir -p "$CA_DIR"/{certs,crl,newcerts,private,requests}
chmod 700 "$CA_DIR/private"
touch "$CA_DIR/index.txt"
echo 1000 > "$CA_DIR/serial"
# Create Root CA configuration
cat > "$ROOT_CONFIG" << 'EOF'
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/ssl/ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
private_key = $dir/private/root-ca.key
certificate = $dir/certs/root-ca.crt
crlnumber = $dir/crlnumber
crl = $dir/crl/root-ca.crl
crl_extensions = crl_ext
default_crl_days = 30
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 3650
preserve = no
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
x509_extensions = v3_ca
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ crl_ext ]
authorityKeyIdentifier=keyid:always
EOF
# Generate Root CA private key
openssl genrsa -aes256 -out "$ROOT_KEY" 4096
chmod 400 "$ROOT_KEY"
# Generate Root CA certificate
openssl req -config "$ROOT_CONFIG" \
-key "$ROOT_KEY" \
-new -x509 -days 7300 -sha256 -extensions v3_ca \
-out "$ROOT_CERT" \
-subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=Root CA"
# Verify Root CA certificate
openssl x509 -noout -text -in "$ROOT_CERT"
Step 2: Create Intermediate Certificate Authority
The intermediate CA will issue certificates for services:
#!/bin/bash
# create-intermediate-ca.sh
# Configuration
CA_DIR="/etc/ssl/ca"
INT_DIR="$CA_DIR/intermediate"
INT_KEY="$INT_DIR/private/intermediate-ca.key"
INT_CSR="$INT_DIR/csr/intermediate-ca.csr"
INT_CERT="$INT_DIR/certs/intermediate-ca.crt"
INT_CONFIG="$INT_DIR/intermediate-ca.conf"
# Create intermediate directory structure
mkdir -p "$INT_DIR"/{certs,crl,csr,newcerts,private}
chmod 700 "$INT_DIR/private"
touch "$INT_DIR/index.txt"
echo 1000 > "$INT_DIR/serial"
echo 1000 > "$INT_DIR/crlnumber"
# Create Intermediate CA configuration
cat > "$INT_CONFIG" << 'EOF'
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/ssl/ca/intermediate
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
private_key = $dir/private/intermediate-ca.key
certificate = $dir/certs/intermediate-ca.crt
crlnumber = $dir/crlnumber
crl = $dir/crl/intermediate-ca.crl
crl_extensions = crl_ext
default_crl_days = 30
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ ocsp ]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning
EOF
# Generate Intermediate CA private key
openssl genrsa -aes256 -out "$INT_KEY" 4096
chmod 400 "$INT_KEY"
# Generate Intermediate CA certificate request
openssl req -config "$INT_CONFIG" -new -sha256 \
-key "$INT_KEY" \
-out "$INT_CSR" \
-subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=Intermediate CA"
# Sign Intermediate CA certificate with Root CA
openssl ca -config "$CA_DIR/root-ca.conf" \
-extensions v3_intermediate_ca \
-days 3650 -notext -md sha256 \
-in "$INT_CSR" \
-out "$INT_CERT"
# Create certificate chain
cat "$INT_CERT" "$CA_DIR/certs/root-ca.crt" > "$INT_DIR/certs/ca-chain.crt"
# Verify certificate chain
openssl verify -CAfile "$CA_DIR/certs/root-ca.crt" "$INT_CERT"
DNS Integration for Certificate Validation
CoreDNS Configuration with ACME Support
Configure CoreDNS to handle DNS challenges for automatic certificate issuance:
# coredns-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-config
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
# Internal zone
file /etc/coredns/zones/internal.zone internal.company.com {
reload 30s
}
# ACME DNS challenge support
file /etc/coredns/zones/acme.zone _acme-challenge.internal.company.com {
reload 5s
}
# Forwarding for external domains
forward . 8.8.8.8 8.8.4.4 {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
# Prometheus metrics
prometheus :9153
# Logging
log . {
class error
}
}
internal.zone: |
$ORIGIN internal.company.com.
$TTL 3600
@ IN SOA ns1.internal.company.com. admin.internal.company.com. (
2024010101 ; Serial
3600 ; Refresh
1800 ; Retry
604800 ; Expire
86400 ; Minimum TTL
)
; Name servers
@ IN NS ns1.internal.company.com.
@ IN NS ns2.internal.company.com.
; A records
ns1 IN A 10.0.1.10
ns2 IN A 10.0.1.11
ca IN A 10.0.1.20
vault IN A 10.0.1.21
; Service records
web IN A 10.0.2.10
api IN A 10.0.2.20
db IN A 10.0.2.30
; CNAME records
www IN CNAME web
acme.zone: |
$ORIGIN _acme-challenge.internal.company.com.
$TTL 60
@ IN SOA ns1.internal.company.com. admin.internal.company.com. (
2024010101 ; Serial
60 ; Refresh
30 ; Retry
3600 ; Expire
60 ; Minimum TTL
)
DNS-01 Challenge Automation
Implement automated DNS-01 challenge handling:
#!/usr/bin/env python3
# dns_challenge_handler.py
import os
import time
import dns.resolver
import dns.update
import dns.tsigkeyring
import dns.query
from flask import Flask, request, jsonify
app = Flask(__name__)
# Configuration
DNS_SERVER = os.environ.get('DNS_SERVER', '10.0.1.10')
DNS_ZONE = os.environ.get('DNS_ZONE', 'internal.company.com')
TSIG_KEY_NAME = os.environ.get('TSIG_KEY_NAME', 'acme-key')
TSIG_KEY = os.environ.get('TSIG_KEY', 'base64-encoded-key')
TSIG_ALGO = os.environ.get('TSIG_ALGO', 'hmac-sha256')
# Create TSIG keyring
keyring = dns.tsigkeyring.from_text({
TSIG_KEY_NAME: TSIG_KEY
})
@app.route('/present', methods=['POST'])
def present_challenge():
"""Add DNS TXT record for ACME challenge"""
data = request.json
domain = data.get('domain')
token = data.get('token')
if not domain or not token:
return jsonify({'error': 'Missing domain or token'}), 400
# Create DNS update
update = dns.update.Update(DNS_ZONE, keyring=keyring, keyalgorithm=TSIG_ALGO)
# Add TXT record
txt_name = f'_acme-challenge.{domain}'
update.add(txt_name, 60, 'TXT', f'"{token}"')
try:
response = dns.query.tcp(update, DNS_SERVER)
# Wait for DNS propagation
time.sleep(5)
# Verify record exists
resolver = dns.resolver.Resolver()
resolver.nameservers = [DNS_SERVER]
answers = resolver.resolve(f'{txt_name}.{DNS_ZONE}', 'TXT')
for rdata in answers:
if token in str(rdata):
return jsonify({'status': 'success'}), 200
return jsonify({'error': 'Record not found after creation'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/cleanup', methods=['POST'])
def cleanup_challenge():
"""Remove DNS TXT record after challenge completion"""
data = request.json
domain = data.get('domain')
if not domain:
return jsonify({'error': 'Missing domain'}), 400
# Create DNS update
update = dns.update.Update(DNS_ZONE, keyring=keyring, keyalgorithm=TSIG_ALGO)
# Delete TXT record
txt_name = f'_acme-challenge.{domain}'
update.delete(txt_name, 'TXT')
try:
response = dns.query.tcp(update, DNS_SERVER)
return jsonify({'status': 'success'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
Automated Certificate Management
Step-CA for ACME Protocol
Deploy Step-CA as an ACME server integrated with your CA:
# step-ca-config.yaml
{
"root": "/etc/step-ca/certs/root-ca.crt",
"federatedRoots": [],
"crt": "/etc/step-ca/certs/intermediate-ca.crt",
"key": "/etc/step-ca/secrets/intermediate-ca.key",
"address": ":443",
"insecureAddress": "",
"dnsNames": ["ca.internal.company.com"],
"logger": { "format": "json" },
"db": { "type": "badgerv2", "dataSource": "/etc/step-ca/db" },
"authority":
{
"provisioners":
[
{
"type": "ACME",
"name": "acme",
"forceCN": true,
"claims":
{
"maxTLSCertDuration": "720h",
"defaultTLSCertDuration": "168h",
},
},
{
"type": "JWK",
"name": "admin",
"key":
{
"use": "sig",
"kty": "EC",
"kid": "admin-key-id",
"crv": "P-256",
"alg": "ES256",
"x": "base64-x-coordinate",
"y": "base64-y-coordinate",
},
"encryptedKey": "encrypted-private-key",
"claims":
{
"maxTLSCertDuration": "8760h",
"defaultTLSCertDuration": "720h",
"enableSSHCA": true,
},
},
],
},
"tls":
{
"cipherSuites":
[
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
],
"minVersion": 1.2,
"maxVersion": 1.3,
"renegotiation": false,
},
}
Cert-Manager Integration
Deploy cert-manager for Kubernetes environments:
# cert-manager-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca-issuer
spec:
acme:
server: https://ca.internal.company.com/acme/acme/directory
email: admin@company.com
privateKeySecretRef:
name: internal-ca-account-key
solvers:
- dns01:
webhook:
groupName: acme.company.com
solverName: internal-dns
config:
endpoint: http://dns-challenge-handler:8080
zone: internal.company.com
---
apiVersion: v1
kind: Secret
metadata:
name: ca-root-cert
namespace: cert-manager
type: Opaque
data:
ca.crt: # base64 encoded root CA certificate
Certificate Lifecycle Automation
#!/bin/bash
# cert-lifecycle-manager.sh
# Configuration
CERT_DIR="/etc/ssl/services"
CA_URL="https://ca.internal.company.com"
RENEWAL_DAYS=30
LOG_FILE="/var/log/cert-manager.log"
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Check certificate expiration
check_cert_expiry() {
local cert_file=$1
local days_until_expiry
if [[ -f "$cert_file" ]]; then
days_until_expiry=$(openssl x509 -enddate -noout -in "$cert_file" | \
cut -d= -f2 | xargs -I {} date -d {} +%s | \
awk -v now=$(date +%s) '{print int(($1-now)/86400)}')
echo "$days_until_expiry"
else
echo "-1"
fi
}
# Request new certificate using ACME
request_certificate() {
local domain=$1
local cert_path="$CERT_DIR/$domain"
log "Requesting certificate for $domain"
# Create certificate directory
mkdir -p "$cert_path"
# Request certificate using step CLI
step ca certificate "$domain" \
"$cert_path/cert.pem" \
"$cert_path/key.pem" \
--ca-url="$CA_URL" \
--root="/etc/ssl/ca/certs/root-ca.crt" \
--acme="$CA_URL/acme/acme/directory" \
--kty=RSA \
--size=2048
if [[ $? -eq 0 ]]; then
log "Certificate successfully obtained for $domain"
# Set proper permissions
chmod 644 "$cert_path/cert.pem"
chmod 600 "$cert_path/key.pem"
# Create combined certificate chain
cat "$cert_path/cert.pem" \
"/etc/ssl/ca/intermediate/certs/intermediate-ca.crt" \
> "$cert_path/fullchain.pem"
return 0
else
log "ERROR: Failed to obtain certificate for $domain"
return 1
fi
}
# Renew certificates
renew_certificates() {
local services_file="/etc/ssl/services.conf"
while IFS= read -r line; do
# Skip comments and empty lines
[[ "$line" =~ ^#.*$ ]] || [[ -z "$line" ]] && continue
# Parse service configuration
domain=$(echo "$line" | cut -d':' -f1)
service=$(echo "$line" | cut -d':' -f2)
reload_cmd=$(echo "$line" | cut -d':' -f3)
cert_file="$CERT_DIR/$domain/cert.pem"
days_left=$(check_cert_expiry "$cert_file")
if [[ $days_left -lt $RENEWAL_DAYS ]]; then
log "Certificate for $domain expires in $days_left days, renewing..."
if request_certificate "$domain"; then
# Reload service if specified
if [[ -n "$reload_cmd" ]]; then
log "Reloading service: $service"
eval "$reload_cmd"
fi
fi
else
log "Certificate for $domain is valid for $days_left more days"
fi
done < "$services_file"
}
# Main execution
main() {
log "Starting certificate lifecycle manager"
# Ensure required directories exist
mkdir -p "$CERT_DIR"
# Run certificate renewal check
renew_certificates
log "Certificate lifecycle check completed"
}
# Run main function
main
Service Configuration Examples
Nginx with Auto-Renewed Certificates
# /etc/nginx/sites-available/secure-app
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name app.internal.company.com;
# Certificate paths
ssl_certificate /etc/ssl/services/app.internal.company.com/fullchain.pem;
ssl_certificate_key /etc/ssl/services/app.internal.company.com/key.pem;
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/services/app.internal.company.com/fullchain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Application configuration
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name app.internal.company.com;
return 301 https://$server_name$request_uri;
}
PostgreSQL with SSL/TLS
# postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/services/db.internal.company.com/cert.pem'
ssl_key_file = '/etc/ssl/services/db.internal.company.com/key.pem'
ssl_ca_file = '/etc/ssl/ca/certs/ca-chain.crt'
ssl_crl_file = ''
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
ssl_prefer_server_ciphers = on
ssl_ecdh_curve = 'prime256v1'
ssl_min_protocol_version = 'TLSv1.2'
ssl_max_protocol_version = ''
# pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
hostssl all all 10.0.0.0/16 cert clientcert=verify-full
hostssl replication replicator 10.0.0.0/16 cert clientcert=verify-full
Client Certificate Management
Generating Client Certificates
#!/bin/bash
# generate-client-cert.sh
CLIENT_NAME="$1"
CLIENT_DIR="/etc/ssl/clients/$CLIENT_NAME"
if [[ -z "$CLIENT_NAME" ]]; then
echo "Usage: $0 <client-name>"
exit 1
fi
# Create client directory
mkdir -p "$CLIENT_DIR"
# Generate client private key
openssl genrsa -out "$CLIENT_DIR/client.key" 2048
chmod 600 "$CLIENT_DIR/client.key"
# Create client certificate request
cat > "$CLIENT_DIR/client.conf" << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = State
L = City
O = Organization
OU = IT
CN = $CLIENT_NAME
emailAddress = $CLIENT_NAME@internal.company.com
[v3_req]
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectAltName = @alt_names
[alt_names]
email = $CLIENT_NAME@internal.company.com
EOF
# Generate certificate request
openssl req -new -key "$CLIENT_DIR/client.key" \
-out "$CLIENT_DIR/client.csr" \
-config "$CLIENT_DIR/client.conf"
# Sign client certificate
openssl ca -config /etc/ssl/ca/intermediate/intermediate-ca.conf \
-extensions usr_cert -days 365 -notext -md sha256 \
-in "$CLIENT_DIR/client.csr" \
-out "$CLIENT_DIR/client.crt"
# Create PKCS12 bundle for easy import
openssl pkcs12 -export \
-out "$CLIENT_DIR/client.p12" \
-inkey "$CLIENT_DIR/client.key" \
-in "$CLIENT_DIR/client.crt" \
-certfile /etc/ssl/ca/intermediate/certs/ca-chain.crt \
-passout pass:changeme
echo "Client certificate generated for $CLIENT_NAME"
echo "Certificate: $CLIENT_DIR/client.crt"
echo "Private Key: $CLIENT_DIR/client.key"
echo "PKCS12 Bundle: $CLIENT_DIR/client.p12"
Monitoring and Maintenance
Certificate Monitoring Dashboard
graph LR
subgraph "Monitoring Stack"
PROM[Prometheus]
ALERT[Alertmanager]
GRAF[Grafana]
end
subgraph "Certificate Checks"
EXP[Expiry Check]
VAL[Validation Check]
REV[Revocation Check]
end
subgraph "Services"
WEB[Web Services]
API[API Services]
DB[Databases]
end
EXP --> PROM
VAL --> PROM
REV --> PROM
WEB --> EXP
API --> EXP
DB --> EXP
PROM --> ALERT
PROM --> GRAF
Prometheus Certificate Exporter
#!/usr/bin/env python3
# cert_exporter.py
import ssl
import socket
import datetime
import time
from prometheus_client import start_http_server, Gauge
# Metrics
cert_expiry_days = Gauge('cert_expiry_days',
'Days until certificate expiry',
['hostname', 'port'])
cert_valid = Gauge('cert_valid',
'Certificate validation status',
['hostname', 'port'])
def check_certificate(hostname, port=443):
"""Check SSL certificate for a given host"""
try:
# Create SSL context
context = ssl.create_default_context()
# Connect and get certificate
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
# Parse expiry date
not_after = datetime.datetime.strptime(
cert['notAfter'],
'%b %d %H:%M:%S %Y %Z'
)
# Calculate days until expiry
days_left = (not_after - datetime.datetime.utcnow()).days
# Update metrics
cert_expiry_days.labels(hostname=hostname, port=port).set(days_left)
cert_valid.labels(hostname=hostname, port=port).set(1)
return days_left
except Exception as e:
print(f"Error checking {hostname}:{port} - {str(e)}")
cert_valid.labels(hostname=hostname, port=port).set(0)
return -1
def main():
# Start Prometheus metrics server
start_http_server(9100)
# Services to monitor
services = [
('web.internal.company.com', 443),
('api.internal.company.com', 443),
('db.internal.company.com', 5432),
]
while True:
for hostname, port in services:
check_certificate(hostname, port)
# Check every 5 minutes
time.sleep(300)
if __name__ == '__main__':
main()
Security Best Practices
1. Key Management
# Secure key storage with proper permissions
chmod 700 /etc/ssl/ca/private
chmod 600 /etc/ssl/ca/private/*.key
# Use hardware security modules (HSM) for production
# Example with SoftHSM
softhsm2-util --init-token --slot 0 --label "CA Keys" \
--pin 1234 --so-pin 5678
# Store CA key in HSM
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
--login --pin 1234 \
--write-object /etc/ssl/ca/private/root-ca.key \
--type privkey --id 01 --label "Root CA Key"
2. Certificate Pinning
// Node.js example with certificate pinning
const https = require("https");
const crypto = require("crypto");
const pinnedCerts = ["sha256//BASE64_ENCODED_CERT_FINGERPRINT"];
const options = {
hostname: "api.internal.company.com",
port: 443,
path: "/",
method: "GET",
checkServerIdentity: (host, cert) => {
const fingerprint = crypto
.createHash("sha256")
.update(cert.raw)
.digest("base64");
if (!pinnedCerts.includes(`sha256//${fingerprint}`)) {
throw new Error("Certificate pin validation failed");
}
},
};
https
.request(options, res => {
// Handle response
})
.end();
3. Audit Logging
#!/bin/bash
# audit-ca-operations.sh
# Enable audit logging for CA operations
cat >> /etc/ssl/ca/intermediate/intermediate-ca.conf << EOF
[ ca ]
# Existing configuration...
audit_log = /var/log/ca-audit.log
EOF
# Log rotation configuration
cat > /etc/logrotate.d/ca-audit << EOF
/var/log/ca-audit.log {
daily
rotate 365
compress
delaycompress
missingok
notifempty
create 0600 root root
sharedscripts
postrotate
# Signal CA service to reopen logs if needed
systemctl reload step-ca 2>/dev/null || true
endscript
}
EOF
Troubleshooting Common Issues
DNS Resolution Issues
# Test DNS resolution
dig _acme-challenge.app.internal.company.com TXT @10.0.1.10
# Verify TSIG key
nsupdate -k /etc/bind/acme-key.key <<EOF
server 10.0.1.10
zone internal.company.com
update add test.internal.company.com 60 A 10.0.0.1
send
EOF
# Check DNS logs
journalctl -u coredns -f
Certificate Validation Problems
# Verify certificate chain
openssl verify -CAfile /etc/ssl/ca/certs/ca-chain.crt \
/etc/ssl/services/app.internal.company.com/cert.pem
# Check certificate details
openssl x509 -in /etc/ssl/services/app.internal.company.com/cert.pem \
-noout -text
# Test SSL connection
openssl s_client -connect app.internal.company.com:443 \
-CAfile /etc/ssl/ca/certs/ca-chain.crt \
-servername app.internal.company.com
Conclusion
Building a secure local SSL infrastructure with DNS integration provides a robust foundation for internal service communication. By combining a proper PKI hierarchy with automated certificate management and DNS-based validation, organizations can achieve enterprise-grade security for their internal networks while maintaining ease of use and automation capabilities.
The key to success is proper planning, automation of routine tasks, and regular monitoring of certificate health. With the tools and configurations provided in this guide, you can implement a production-ready SSL infrastructure that scales with your organization’s needs.