#!/bin/bash ################################################ #### SSL Certificate Deployer #### #### Deploy certs to multiple services #### #### #### #### Author: Phil Connor #### #### License: MIT #### #### Contact: contact@mylinux.work #### #### Version: 1.00-030326 #### ################################################ set -o pipefail SCRIPT_NAME=$(basename "$0") readonly SCRIPT_NAME # Runtime variables CERT_FILE="" KEY_FILE="" CA_FILE="" TARGETS="" DRY_RUN=false BACKUP=false DEBUG=${DEBUG:-} handle_error() { local exit_code=$1 local line_number=$2 echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2 exit "$exit_code" } trap 'handle_error $? $LINENO' ERR debug_echo() { if [[ -n "$DEBUG" ]]; then echo "[DEBUG] $*" >&2 fi } info() { echo "[INFO] $*" } warn() { echo "[WARN] $*" >&2 } error() { echo "[ERROR] $*" >&2 } show_help() { cat << EOF Usage: $SCRIPT_NAME [OPTIONS] Deploy SSL certificates to multiple service targets in a single run. OPTIONS: --cert FILE Path to the SSL certificate file (required) --key FILE Path to the SSL private key file (required) --ca FILE Path to the CA bundle file (optional) --targets LIST Comma-separated list of targets (required) --dry-run Show what would be done without making changes --backup Backup existing certificates before overwriting --help, -h Show this help message SUPPORTED TARGETS: nginx Copy cert+key to /etc/nginx/ssl/, reload nginx apache Copy cert+key to /etc/httpd/ssl/ or /etc/apache2/ssl/, reload postfix Update TLS cert/key in main.cf, reload postfix dovecot Update ssl_cert/ssl_key in dovecot config, reload dovecot artifactory Import cert into Artifactory Java keystore, restart bitbucket Import cert into Bitbucket Java keystore, restart jira Import cert into Jira Java keystore, restart haproxy Concatenate cert+key into PEM at /etc/haproxy/certs/, reload system Update system CA trust store ENVIRONMENT VARIABLES: DEBUG Enable debug output when set EXAMPLES: $SCRIPT_NAME --cert server.crt --key server.key --targets nginx,haproxy $SCRIPT_NAME --cert server.crt --key server.key --ca ca-bundle.crt --targets apache,postfix,dovecot $SCRIPT_NAME --cert server.crt --key server.key --targets artifactory,bitbucket,jira --backup $SCRIPT_NAME --cert server.crt --key server.key --targets system --dry-run DEBUG=1 $SCRIPT_NAME --cert server.crt --key server.key --targets nginx EOF } validate_cert_key_match() { local cert="$1" local key="$2" local cert_modulus cert_modulus=$(openssl x509 -noout -modulus -in "$cert" 2>/dev/null | openssl md5) local key_modulus key_modulus=$(openssl rsa -noout -modulus -in "$key" 2>/dev/null | openssl md5) if [[ "$cert_modulus" != "$key_modulus" ]]; then error "Certificate and key do not match (modulus mismatch)" debug_echo "Cert modulus: $cert_modulus" debug_echo "Key modulus: $key_modulus" return 1 fi debug_echo "Certificate and key match" return 0 } backup_file() { local file="$1" if [[ -f "$file" ]]; then local backup_name backup_name="${file}.bak.$(date +%Y%m%d%H%M%S)" if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would backup $file -> $backup_name" else cp -a "$file" "$backup_name" info "Backed up $file -> $backup_name" fi fi } copy_file() { local src="$1" local dest="$2" if [[ "$BACKUP" == true ]]; then backup_file "$dest" fi if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would copy $src -> $dest" else cp -a "$src" "$dest" chmod 600 "$dest" info "Copied $src -> $dest" fi } reload_service() { local service="$1" if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would reload $service" else if systemctl is-active --quiet "$service" 2>/dev/null; then systemctl reload "$service" info "Reloaded $service" else warn "Service $service is not active, skipping reload" fi fi } restart_service() { local service="$1" if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would restart $service" else systemctl restart "$service" info "Restarted $service" fi } get_keystore_password() { local password_url="$1" local storepass="" # Try Vault HTTP API first if URL provided if [[ -n "$password_url" ]]; then debug_echo "Retrieving keystore password from $password_url" storepass=$(curl -sf -X GET "$password_url" 2>/dev/null | jq -r '.data.password // empty' 2>/dev/null || true) fi # Fall back to Vault CLI if [[ -z "$storepass" ]]; then debug_echo "Falling back to Vault CLI for keystore password" storepass=$(vault kv get -field=password secret/keystore 2>/dev/null || true) fi # Fall back to default if [[ -z "$storepass" ]]; then debug_echo "Using default keystore password" storepass="changeit" fi echo "$storepass" } find_java_keystore() { local -n java_bin_ref=$1 local -n keystore_ref=$2 # Common Java installation paths local java_paths=( "/opt/jfrog/artifactory/app/third-party/java" "/mnt/ebs/bitbucket/*/jre" "/mnt/ebs/jira/jre" "/usr/lib/jvm/java-*-openjdk" "/usr/lib/jvm/default-java" "/opt/java" "/usr/java/latest" ) # Check JAVA_HOME first if [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/keytool" ]]; then java_bin_ref="$JAVA_HOME/bin" keystore_ref="$JAVA_HOME/lib/security/cacerts" if [[ -f "$keystore_ref" ]]; then debug_echo "Found Java via JAVA_HOME: $java_bin_ref" return 0 fi fi # Search common paths with glob expansion for path_pattern in "${java_paths[@]}"; do for java_dir in $path_pattern; do if [[ -d "$java_dir" ]]; then local bin_dir="$java_dir/bin" local cacerts="$java_dir/lib/security/cacerts" if [[ -x "$bin_dir/keytool" && -f "$cacerts" ]]; then java_bin_ref="$bin_dir" keystore_ref="$cacerts" debug_echo "Found Java at: $java_dir" return 0 fi fi done done # Fallback: try system keytool if command -v keytool >/dev/null 2>&1; then java_bin_ref="$(dirname "$(command -v keytool)")" # Try common system keystore locations local system_keystores=( "/etc/ssl/certs/java/cacerts" "/usr/lib/jvm/default-java/lib/security/cacerts" "/etc/pki/ca-trust/extracted/java/cacerts" ) for ks in "${system_keystores[@]}"; do if [[ -f "$ks" ]]; then keystore_ref="$ks" debug_echo "Found system Java at: $java_bin_ref" return 0 fi done fi return 1 } deploy_java_keystore() { local keystore="$1" local java_bin="$2" local alias_name="$3" local vault_url="$4" local service_name="$5" local storepass storepass=$(get_keystore_password "$vault_url") if [[ "$BACKUP" == true ]]; then backup_file "$keystore" fi if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would delete alias '$alias_name' from keystore $keystore" info "[DRY RUN] Would import $CERT_FILE into keystore $keystore" info "[DRY RUN] Would restart $service_name" else "$java_bin/keytool" -delete -alias "$alias_name" -keystore "$keystore" -storepass "$storepass" 2>/dev/null || true "$java_bin/keytool" -import -noprompt -alias "$alias_name" -keystore "$keystore" -file "$CERT_FILE" -storepass "$storepass" info "Imported certificate into $keystore" restart_service "$service_name" fi } # ---- Target handlers ---- deploy_nginx() { info "Deploying to nginx..." local ssl_dir="/etc/nginx/ssl" if [[ "$DRY_RUN" != true ]]; then mkdir -p "$ssl_dir" fi copy_file "$CERT_FILE" "$ssl_dir/server.crt" copy_file "$KEY_FILE" "$ssl_dir/server.key" if [[ -n "$CA_FILE" ]]; then copy_file "$CA_FILE" "$ssl_dir/ca-bundle.crt" fi reload_service nginx } deploy_apache() { info "Deploying to apache..." local ssl_dir="" if [[ -d "/etc/httpd" ]]; then ssl_dir="/etc/httpd/ssl" elif [[ -d "/etc/apache2" ]]; then ssl_dir="/etc/apache2/ssl" else error "Could not detect Apache configuration directory" return 1 fi if [[ "$DRY_RUN" != true ]]; then mkdir -p "$ssl_dir" fi copy_file "$CERT_FILE" "$ssl_dir/server.crt" copy_file "$KEY_FILE" "$ssl_dir/server.key" if [[ -n "$CA_FILE" ]]; then copy_file "$CA_FILE" "$ssl_dir/ca-bundle.crt" fi # Detect and reload the correct service if systemctl list-units --type=service --all 2>/dev/null | grep -q "httpd.service"; then reload_service httpd elif systemctl list-units --type=service --all 2>/dev/null | grep -q "apache2.service"; then reload_service apache2 else warn "Could not detect Apache service name" fi } deploy_postfix() { info "Deploying to postfix..." local main_cf="/etc/postfix/main.cf" if [[ ! -f "$main_cf" ]]; then error "Postfix main.cf not found at $main_cf" return 1 fi if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would update smtpd_tls_cert_file in $main_cf to $CERT_FILE" info "[DRY RUN] Would update smtpd_tls_key_file in $main_cf to $KEY_FILE" info "[DRY RUN] Would reload postfix" else if [[ "$BACKUP" == true ]]; then backup_file "$main_cf" fi if grep -q "^smtpd_tls_cert_file" "$main_cf"; then sed -i "s|^smtpd_tls_cert_file.*|smtpd_tls_cert_file = $CERT_FILE|" "$main_cf" else echo "smtpd_tls_cert_file = $CERT_FILE" >> "$main_cf" fi if grep -q "^smtpd_tls_key_file" "$main_cf"; then sed -i "s|^smtpd_tls_key_file.*|smtpd_tls_key_file = $KEY_FILE|" "$main_cf" else echo "smtpd_tls_key_file = $KEY_FILE" >> "$main_cf" fi info "Updated $main_cf with certificate paths" reload_service postfix fi } deploy_dovecot() { info "Deploying to dovecot..." local dovecot_conf="" if [[ -f "/etc/dovecot/conf.d/10-ssl.conf" ]]; then dovecot_conf="/etc/dovecot/conf.d/10-ssl.conf" elif [[ -f "/etc/dovecot/dovecot.conf" ]]; then dovecot_conf="/etc/dovecot/dovecot.conf" else error "Could not find dovecot configuration" return 1 fi if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would update ssl_cert in $dovecot_conf to <$CERT_FILE" info "[DRY RUN] Would update ssl_key in $dovecot_conf to <$KEY_FILE" info "[DRY RUN] Would reload dovecot" else if [[ "$BACKUP" == true ]]; then backup_file "$dovecot_conf" fi if grep -q "^ssl_cert" "$dovecot_conf"; then sed -i "s|^ssl_cert.*|ssl_cert = <$CERT_FILE|" "$dovecot_conf" else echo "ssl_cert = <$CERT_FILE" >> "$dovecot_conf" fi if grep -q "^ssl_key" "$dovecot_conf"; then sed -i "s|^ssl_key.*|ssl_key = <$KEY_FILE|" "$dovecot_conf" else echo "ssl_key = <$KEY_FILE" >> "$dovecot_conf" fi info "Updated $dovecot_conf with certificate paths" reload_service dovecot fi } deploy_artifactory() { info "Deploying to artifactory..." local java_bin="/opt/jfrog/artifactory/app/third-party/java/bin" local keystore="/opt/jfrog/artifactory/app/third-party/java/lib/security/cacerts" if [[ ! -x "$java_bin/keytool" || ! -f "$keystore" ]]; then debug_echo "Artifactory default paths not found, searching for Java" if ! find_java_keystore java_bin keystore; then error "Could not find Java keytool or keystore for Artifactory" return 1 fi fi deploy_java_keystore "$keystore" "$java_bin" "ssl-cert" "" "artifactory" } deploy_bitbucket() { info "Deploying to bitbucket..." local java_bin="" local keystore="" # Check app-specific paths first with glob for bb_dir in /mnt/ebs/bitbucket/*/jre; do if [[ -d "$bb_dir" && -x "$bb_dir/bin/keytool" && -f "$bb_dir/lib/security/cacerts" ]]; then java_bin="$bb_dir/bin" keystore="$bb_dir/lib/security/cacerts" break fi done if [[ -z "$java_bin" || -z "$keystore" ]]; then debug_echo "Bitbucket default paths not found, searching for Java" if ! find_java_keystore java_bin keystore; then error "Could not find Java keytool or keystore for Bitbucket" return 1 fi fi deploy_java_keystore "$keystore" "$java_bin" "ssl-cert" "" "atlbitbucket" } deploy_jira() { info "Deploying to jira..." local java_bin="/mnt/ebs/jira/jre/bin" local keystore="/mnt/ebs/jira/jre/lib/security/cacerts" if [[ ! -x "$java_bin/keytool" || ! -f "$keystore" ]]; then debug_echo "Jira default paths not found, searching for Java" if ! find_java_keystore java_bin keystore; then error "Could not find Java keytool or keystore for Jira" return 1 fi fi deploy_java_keystore "$keystore" "$java_bin" "ssl-cert" "" "jira" } deploy_haproxy() { info "Deploying to haproxy..." local cert_dir="/etc/haproxy/certs" local pem_file="$cert_dir/server.pem" if [[ "$DRY_RUN" != true ]]; then mkdir -p "$cert_dir" fi if [[ "$BACKUP" == true ]]; then backup_file "$pem_file" fi if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would concatenate $CERT_FILE + $KEY_FILE -> $pem_file" info "[DRY RUN] Would reload haproxy" else cat "$CERT_FILE" "$KEY_FILE" > "$pem_file" chmod 600 "$pem_file" info "Created combined PEM at $pem_file" reload_service haproxy fi } deploy_system() { info "Deploying to system CA trust store..." if [[ -z "$CA_FILE" && -z "$CERT_FILE" ]]; then error "No certificate or CA bundle provided for system trust store" return 1 fi local cert_to_install="${CA_FILE:-$CERT_FILE}" if command -v update-ca-trust >/dev/null 2>&1; then # RHEL/CentOS/Fedora/Rocky/Alma local trust_dir="/etc/pki/ca-trust/source/anchors" local cert_name cert_name=$(basename "$cert_to_install") if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would copy $cert_to_install -> $trust_dir/$cert_name" info "[DRY RUN] Would run update-ca-trust" else copy_file "$cert_to_install" "$trust_dir/$cert_name" update-ca-trust info "Updated system CA trust store (RHEL-based)" fi elif command -v update-ca-certificates >/dev/null 2>&1; then # Debian/Ubuntu local trust_dir="/usr/local/share/ca-certificates" local cert_name cert_name=$(basename "$cert_to_install") # Debian requires .crt extension cert_name="${cert_name%.*}.crt" if [[ "$DRY_RUN" == true ]]; then info "[DRY RUN] Would copy $cert_to_install -> $trust_dir/$cert_name" info "[DRY RUN] Would run update-ca-certificates" else copy_file "$cert_to_install" "$trust_dir/$cert_name" update-ca-certificates info "Updated system CA trust store (Debian-based)" fi else error "Could not find update-ca-trust or update-ca-certificates" return 1 fi } parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in --cert) CERT_FILE="$2" shift 2 ;; --key) KEY_FILE="$2" shift 2 ;; --ca) CA_FILE="$2" shift 2 ;; --targets) TARGETS="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --backup) BACKUP=true shift ;; --help|-h) show_help exit 0 ;; *) error "Unknown option: $1" show_help >&2 exit 1 ;; esac done } validate_inputs() { if [[ -z "$CERT_FILE" ]]; then error "Certificate file is required (--cert)" exit 1 fi if [[ -z "$KEY_FILE" ]]; then error "Key file is required (--key)" exit 1 fi if [[ -z "$TARGETS" ]]; then error "At least one target is required (--targets)" exit 1 fi if [[ ! -f "$CERT_FILE" ]]; then error "Certificate file not found: $CERT_FILE" exit 1 fi if [[ ! -f "$KEY_FILE" ]]; then error "Key file not found: $KEY_FILE" exit 1 fi if [[ -n "$CA_FILE" && ! -f "$CA_FILE" ]]; then error "CA bundle file not found: $CA_FILE" exit 1 fi if ! openssl x509 -noout -text -in "$CERT_FILE" >/dev/null 2>&1; then error "Invalid certificate file: $CERT_FILE" exit 1 fi if ! openssl rsa -noout -check -in "$KEY_FILE" >/dev/null 2>&1; then error "Invalid key file: $KEY_FILE" exit 1 fi if ! validate_cert_key_match "$CERT_FILE" "$KEY_FILE"; then exit 1 fi } deploy_target() { local target="$1" case "$target" in nginx) deploy_nginx ;; apache) deploy_apache ;; postfix) deploy_postfix ;; dovecot) deploy_dovecot ;; artifactory) deploy_artifactory ;; bitbucket) deploy_bitbucket ;; jira) deploy_jira ;; haproxy) deploy_haproxy ;; system) deploy_system ;; *) error "Unknown target: $target" error "Valid targets: nginx, apache, postfix, dovecot, artifactory, bitbucket, jira, haproxy, system" return 1 ;; esac } main() { parse_arguments "$@" validate_inputs if [[ "$DRY_RUN" == true ]]; then info "Running in DRY RUN mode — no changes will be made" fi local failed=0 local succeeded=0 IFS=',' read -ra target_list <<< "$TARGETS" for target in "${target_list[@]}"; do # Trim whitespace target=$(echo "$target" | tr -d '[:space:]') info "--- Deploying to target: $target ---" if deploy_target "$target"; then ((succeeded++)) info "Target $target: OK" else ((failed++)) error "Target $target: FAILED" fi echo done info "Deployment complete: $succeeded succeeded, $failed failed" if [[ $failed -gt 0 ]]; then return 1 fi } # Execute main function if script is run directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi