commit 576e7de917da8c508787f312946f365ece74f3dd Author: root Date: Thu Oct 23 08:11:35 2025 +0200 Initial commit: Certificate management tools - cert-manager.py: Interactive certificate lifecycle management - generate-csr.sh: Generate CSR on remote host - sign-cert.sh: Sign certificate with UCS CA - README.md: Complete documentation - .gitignore: Ignore certificate and config files Features: - Interactive prompts with default values - Config persistence between runs - Remote CSR generation with proper server extensions - Automated CA signing - Optional certificate deployment diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..709bf64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Certificate files +*.req +*.csr +*.pem +*.crt +*.key + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Config files +.cert-manager-config.json + +# OS files +.DS_Store +Thumbs.db + +# Editor files +*.swp +*.swo +*~ +.vscode/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0115b39 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Certificate Management Tools + +Automated certificate generation and signing tools for UCS CA. + +## Tools + +### 1. cert-manager.py (Interactive Mode) +The main interactive tool that handles the entire certificate lifecycle. + +**Usage:** +```bash +./cert-manager.py +``` + +**Features:** +- Interactive prompts with default values +- Remembers last used values +- Generates CSR on remote host +- Signs certificate with UCS CA +- Optionally deploys certificate back to target host + +### 2. generate-csr.sh (Standalone) +Generates a certificate signing request on a remote host. + +**Usage:** +```bash +./generate-csr.sh [country] [state] [locality] [org] [ou] +``` + +**Example:** +```bash +./generate-csr.sh 192.168.1.100 server.example.com DE berlin berlin egonetix it +``` + +### 3. sign-cert.sh (Standalone) +Signs a certificate request with the UCS CA. + +**Usage:** +```bash +./sign-cert.sh [days] +``` + +**Example:** +```bash +./sign-cert.sh server.req server 3650 +``` + +## Configuration + +The interactive tool stores default values in `~/.cert-manager-config.json`. + +Default values: +- Country: DE +- State: berlin +- Locality: berlin +- Organization: egonetix +- Organizational Unit: it +- CA Server: 10.0.0.21 +- Validity: 3650 days (10 years) + +## Workflow + +1. Run `./cert-manager.py` +2. Enter target host (IP or hostname where certificate will be used) +3. Enter common name (FQDN for the certificate) +4. Review/modify certificate subject fields +5. Confirm and proceed +6. The tool will: + - Generate CSR on target host + - Sign it with UCS CA + - Optionally copy certificate back to target + +## Requirements + +- SSH access to target host as root +- SSH access to UCS CA server (10.0.0.21) as root +- OpenSSL on target host +- Python 3.6+ for interactive tool + +## Notes + +- Private keys are generated and remain on the target host +- Certificate requests (.req) and signed certificates (-cert.pem) are stored locally +- The interactive tool remembers your last target host and common name for convenience diff --git a/cert-manager.py b/cert-manager.py new file mode 100755 index 0000000..befe276 --- /dev/null +++ b/cert-manager.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +Interactive Certificate Manager +Generates CSR on remote host and signs it with UCS CA +""" + +import os +import sys +import json +import subprocess +from pathlib import Path + +# Configuration +CONFIG_FILE = Path.home() / '.cert-manager-config.json' +DEFAULT_CONFIG = { + 'country': 'DE', + 'state': 'berlin', + 'locality': 'berlin', + 'organization': 'egonetix', + 'organizational_unit': 'it', + 'ca_server': '10.0.0.21', + 'validity_days': '3650', + 'last_target_host': '', + 'last_common_name': '' +} + +def load_config(): + """Load configuration from file or return defaults""" + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + # Merge with defaults to add any new fields + return {**DEFAULT_CONFIG, **config} + except Exception as e: + print(f"Warning: Could not load config: {e}") + return DEFAULT_CONFIG.copy() + +def save_config(config): + """Save configuration to file""" + try: + with open(CONFIG_FILE, 'w') as f: + json.dump(config, f, indent=2) + except Exception as e: + print(f"Warning: Could not save config: {e}") + +def prompt_with_default(prompt, default): + """Prompt user with a default value""" + if default: + user_input = input(f"{prompt} [{default}]: ").strip() + return user_input if user_input else default + else: + return input(f"{prompt}: ").strip() + +def yes_no_prompt(prompt, default=True): + """Ask a yes/no question""" + default_str = "Y/n" if default else "y/N" + while True: + response = input(f"{prompt} [{default_str}]: ").strip().lower() + if not response: + return default + if response in ['y', 'yes']: + return True + if response in ['n', 'no']: + return False + print("Please answer 'y' or 'n'") + +def main(): + print("=" * 60) + print("Interactive Certificate Manager") + print("=" * 60) + print() + + # Load config + config = load_config() + + # Ask if user wants to modify defaults + if CONFIG_FILE.exists(): + if yes_no_prompt("Do you want to modify default values?", False): + print("\n--- Default Values Configuration ---") + config['country'] = prompt_with_default("Country (C)", config['country']) + config['state'] = prompt_with_default("State/Province (ST)", config['state']) + config['locality'] = prompt_with_default("Locality (L)", config['locality']) + config['organization'] = prompt_with_default("Organization (O)", config['organization']) + config['organizational_unit'] = prompt_with_default("Organizational Unit (OU)", config['organizational_unit']) + config['ca_server'] = prompt_with_default("CA Server", config['ca_server']) + config['validity_days'] = prompt_with_default("Validity (days)", config['validity_days']) + print() + + # Get certificate details + print("--- Certificate Details ---") + target_host = prompt_with_default("Target Host (IP or hostname)", config['last_target_host']) + + if not target_host: + print("Error: Target host is required!") + sys.exit(1) + + common_name = prompt_with_default("Common Name (FQDN)", config['last_common_name']) + + if not common_name: + print("Error: Common name is required!") + sys.exit(1) + + # Extract short name for filenames + short_name = common_name.split('.')[0] + + # Ask for custom values for this certificate + print("\n--- Certificate Subject (press Enter to use defaults) ---") + country = prompt_with_default("Country (C)", config['country']) + state = prompt_with_default("State/Province (ST)", config['state']) + locality = prompt_with_default("Locality (L)", config['locality']) + organization = prompt_with_default("Organization (O)", config['organization']) + org_unit = prompt_with_default("Organizational Unit (OU)", config['organizational_unit']) + validity_days = prompt_with_default("Validity (days)", config['validity_days']) + + print("\n" + "=" * 60) + print("Summary:") + print("=" * 60) + print(f"Target Host: {target_host}") + print(f"Common Name: {common_name}") + print(f"Country: {country}") + print(f"State: {state}") + print(f"Locality: {locality}") + print(f"Organization: {organization}") + print(f"Org Unit: {org_unit}") + print(f"Validity: {validity_days} days") + print(f"CA Server: {config['ca_server']}") + print(f"Output files: {short_name}.req, {short_name}-cert.pem") + print("=" * 60) + print() + + if not yes_no_prompt("Proceed with certificate generation?", True): + print("Cancelled.") + sys.exit(0) + + # Save config for next run + config['last_target_host'] = target_host + config['last_common_name'] = common_name + save_config(config) + + # Get script directory + script_dir = Path(__file__).parent.absolute() + + print("\n" + "=" * 60) + print("Step 1: Generating CSR on target host") + print("=" * 60) + + # Run generate-csr.sh + generate_cmd = [ + str(script_dir / 'generate-csr.sh'), + target_host, + common_name, + country, + state, + locality, + organization, + org_unit + ] + + try: + result = subprocess.run(generate_cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"\nError: CSR generation failed with exit code {e.returncode}") + sys.exit(1) + except FileNotFoundError: + print(f"\nError: generate-csr.sh not found in {script_dir}") + sys.exit(1) + + print("\n" + "=" * 60) + print("Step 2: Signing certificate with CA") + print("=" * 60) + + # Run sign-cert.sh + req_file = f"{short_name}.req" + sign_cmd = [ + str(script_dir / 'sign-cert.sh'), + req_file, + short_name, + validity_days + ] + + try: + result = subprocess.run(sign_cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"\nError: Certificate signing failed with exit code {e.returncode}") + sys.exit(1) + except FileNotFoundError: + 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) + + cert_file = f"{short_name}-cert.pem" + + if yes_no_prompt("Do you want to copy the certificate back to the target host?", True): + try: + # Copy certificate to target + subprocess.run(['scp', cert_file, f'root@{target_host}:/tmp/{short_name}.crt'], check=True) + print(f"\nāœ“ Certificate copied to target host at /tmp/{short_name}.crt") + print(f" Private key is at /tmp/{short_name}.key") + except subprocess.CalledProcessError: + print("\nWarning: Failed to copy certificate to target host") + + print("\n" + "=" * 60) + print("āœ“ Certificate Management Complete!") + print("=" * 60) + print(f"\nFiles created:") + print(f" - {req_file} (Certificate Request)") + print(f" - {cert_file} (Signed Certificate)") + print(f"\nOn target host ({target_host}):") + print(f" - /tmp/{short_name}.key (Private Key)") + print(f" - /tmp/{short_name}.crt (Certificate)") + print("\n") + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print("\n\nCancelled by user.") + sys.exit(1) + except Exception as e: + print(f"\nError: {e}") + sys.exit(1) diff --git a/generate-csr.sh b/generate-csr.sh new file mode 100755 index 0000000..f99f750 --- /dev/null +++ b/generate-csr.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# Script to generate a certificate request on a remote host +# Usage: ./generate-csr.sh [country] [state] [locality] [org] [ou] + +set -e + +# Check arguments +if [ $# -lt 2 ]; then + echo "Usage: $0 [country] [state] [locality] [org] [ou]" + echo "" + echo "Example: $0 192.168.1.100 myserver.domain.com DE berlin berlin egonetix it" + exit 1 +fi + +TARGET_HOST="$1" +COMMON_NAME="$2" +COUNTRY="${3:-DE}" +STATE="${4:-berlin}" +LOCALITY="${5:-berlin}" +ORG="${6:-egonetix}" +OU="${7:-it}" + +# Extract short hostname from common name +SHORT_NAME=$(echo "$COMMON_NAME" | cut -d'.' -f1) +OUTPUT_FILE="${SHORT_NAME}.req" + +echo "==========================================" +echo "Certificate Request Generation" +echo "==========================================" +echo "Target host: $TARGET_HOST" +echo "Common Name: $COMMON_NAME" +echo "Country: $COUNTRY" +echo "State: $STATE" +echo "Locality: $LOCALITY" +echo "Organization: $ORG" +echo "Org Unit: $OU" +echo "Output file: $OUTPUT_FILE" +echo "==========================================" +echo "" + +# Create OpenSSL config +CONFIG_CONTENT="[req] +default_bits = 4096 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = v3_req + +[dn] +C=$COUNTRY +ST=$STATE +L=$LOCALITY +O=$ORG +OU=$OU +CN=$COMMON_NAME + +[v3_req] +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = $COMMON_NAME +DNS.2 = $SHORT_NAME" + +# Add alternative names if common name contains domain +if [[ "$COMMON_NAME" == *.* ]]; then + CONFIG_CONTENT="$CONFIG_CONTENT +DNS.3 = ${SHORT_NAME}.${COMMON_NAME#*.}" +fi + +echo "[1/4] Creating OpenSSL configuration..." +echo "$CONFIG_CONTENT" > /tmp/csr_config.conf + +echo "[2/4] Copying config to target host..." +scp /tmp/csr_config.conf root@${TARGET_HOST}:/tmp/csr_config.conf +if [ $? -ne 0 ]; then + echo "Error: Failed to copy config to target host" + exit 1 +fi + +echo "[3/4] Generating CSR on target host..." +ssh root@${TARGET_HOST} "openssl req -new -newkey rsa:4096 -nodes -keyout /tmp/${SHORT_NAME}.key -out /tmp/${SHORT_NAME}.csr -config /tmp/csr_config.conf" +if [ $? -ne 0 ]; then + echo "Error: Failed to generate CSR on target host" + exit 1 +fi + +echo "[4/4] Downloading CSR..." +scp root@${TARGET_HOST}:/tmp/${SHORT_NAME}.csr "$OUTPUT_FILE" +if [ $? -ne 0 ]; then + echo "Error: Failed to download CSR" + exit 1 +fi + +# Clean up local temp file +rm -f /tmp/csr_config.conf + +echo "" +echo "==========================================" +echo "āœ“ CSR generated successfully!" +echo "==========================================" +echo "Certificate request saved to: $OUTPUT_FILE" +echo "" +echo "CSR details:" +openssl req -in "$OUTPUT_FILE" -noout -text | grep -A 10 "Subject:" +echo "" +echo "IMPORTANT: Private key is stored on target host at:" +echo " /tmp/${SHORT_NAME}.key" +echo "" +echo "Next step: Sign this CSR with:" +echo " ./sign-cert.sh $OUTPUT_FILE $SHORT_NAME" +echo "==========================================" diff --git a/sign-cert.sh b/sign-cert.sh new file mode 100755 index 0000000..e8c3975 --- /dev/null +++ b/sign-cert.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Script to sign a certificate request with UCS CA +# Usage: ./sign-cert.sh [days] + +set -e + +# Configuration +UCS_SERVER="10.0.0.21" +UCS_USER="root" +DEFAULT_DAYS=3650 + +# Check arguments +if [ $# -lt 2 ]; then + echo "Usage: $0 [days]" + echo "" + echo "Example: $0 webui.req myserver 3650" + echo "" + echo "The script will:" + echo " 1. Copy the CSR to UCS server" + echo " 2. Sign it with the UCS CA" + echo " 3. Download the signed certificate to current directory" + exit 1 +fi + +REQ_FILE="$1" +HOSTNAME="$2" +DAYS="${3:-$DEFAULT_DAYS}" + +# Validate req file exists +if [ ! -f "$REQ_FILE" ]; then + echo "Error: Certificate request file '$REQ_FILE' not found!" + exit 1 +fi + +# Get absolute path of req file +REQ_FILE=$(realpath "$REQ_FILE") +OUTPUT_FILE="${HOSTNAME}-cert.pem" + +echo "==========================================" +echo "UCS Certificate Signing Script" +echo "==========================================" +echo "Request file: $REQ_FILE" +echo "Hostname: $HOSTNAME" +echo "Valid days: $DAYS" +echo "Output file: $OUTPUT_FILE" +echo "==========================================" +echo "" + +# Step 1: Copy CSR to UCS server +echo "[1/3] Copying CSR to UCS server..." +scp "$REQ_FILE" ${UCS_USER}@${UCS_SERVER}:/tmp/${HOSTNAME}.csr +if [ $? -ne 0 ]; then + echo "Error: Failed to copy CSR to UCS server" + exit 1 +fi + +# Step 2: Sign the certificate +echo "[2/3] Signing certificate on UCS server..." +ssh ${UCS_USER}@${UCS_SERVER} "univention-certificate sign -request /tmp/${HOSTNAME}.csr -name ${HOSTNAME} -days ${DAYS}" +if [ $? -ne 0 ]; then + echo "Error: Failed to sign certificate" + exit 1 +fi + +# Step 3: Download signed certificate +echo "[3/3] Downloading signed certificate..." +scp ${UCS_USER}@${UCS_SERVER}:/etc/univention/ssl/${HOSTNAME}/cert.pem "$OUTPUT_FILE" +if [ $? -ne 0 ]; then + echo "Error: Failed to download signed certificate" + exit 1 +fi + +echo "" +echo "==========================================" +echo "āœ“ Certificate signed successfully!" +echo "==========================================" +echo "Certificate saved to: $OUTPUT_FILE" +echo "" +echo "Certificate details:" +openssl x509 -in "$OUTPUT_FILE" -noout -subject -issuer -dates +echo "" +echo "Subject Alternative Names:" +openssl x509 -in "$OUTPUT_FILE" -noout -text | grep -A 1 "Subject Alternative Name" | tail -1 +echo "" +echo "Extended Key Usage:" +openssl x509 -in "$OUTPUT_FILE" -noout -text | grep -A 1 "Extended Key Usage" | tail -1 +echo "=========================================="