Files
linux-scripts/ssl-cert-deploy.sh
T

683 lines
19 KiB
Bash

#!/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