#!/usr/bin/env bash # Simple local CA helper for issuing server certs with SANs, verifying, and optional nginx deploy. set -euo pipefail MYCA_DIR="${MYCA_DIR:-/etc/ssl/myca}" CERT_DIR="$MYCA_DIR/certs" KEY_DIR="$MYCA_DIR/private" CSR_DIR="$MYCA_DIR/csrs" EXT_DIR="$MYCA_DIR/exts" NGINX_SSL_DIR="${NGINX_SSL_DIR:-/etc/nginx/ssl}" usage() { cat <<'USAGE' myca.sh COMMAND [args] Commands: issue NAME [--dns foo --ip 1.2.3.4 ...] [--days N] [--reuse-key] [--deploy-nginx] Create key + CSR + SAN ext, sign with CA, write cert. sign NAME --csr /path/to/file.csr [--dns ... --ip ...] [--days N] Sign an existing CSR with SANs. show NAME|/path/to/cert.crt Show subject, SANs, validity. verify NAME|/path/to/cert.crt Verify against CA. list List issued certs. trust-ca Install CA into Debian or Ubuntu trust store and update. Environment: MYCA_DIR Default /etc/ssl/myca NGINX_SSL_DIR Default /etc/nginx/ssl Examples: myca.sh issue snipe-it.lan --dns snipe-it.lan --ip 192.168.17.51 --deploy-nginx myca.sh show snipe-it.lan myca.sh verify snipe-it.lan Notes: - CA files expected: $MYCA_DIR/myCA.pem and $MYCA_DIR/myCA.key - Avoid .local hostnames. Use .lan or a domain you control. USAGE } ensure_dirs() { install -d -m 755 "$CERT_DIR" "$CSR_DIR" "$EXT_DIR" install -d -m 700 "$KEY_DIR" } have_ca() { [[ -s "$MYCA_DIR/myCA.pem" && -s "$MYCA_DIR/myCA.key" ]] || { echo "Missing CA: $MYCA_DIR/myCA.pem or myCA.key" >&2; exit 1; } } make_ext() { local name="$1"; shift local dns_arr=() ip_arr=() while [[ $# -gt 0 ]]; do case "$1" in --dns) dns_arr+=("$2"); shift 2;; --ip) ip_arr+=("$2"); shift 2;; *) echo "Unknown ext arg: $1" >&2; exit 2;; case_esac esac done local has_cn=0 for d in "${dns_arr[@]:-}"; do [[ "$d" == "$name" ]] && has_cn=1; done [[ $has_cn -eq 0 ]] && dns_arr=("$name" "${dns_arr[@]:-}") { echo "basicConstraints=CA:FALSE" echo "keyUsage=digitalSignature,keyEncipherment" echo "extendedKeyUsage=serverAuth" echo "subjectAltName=@alt_names" echo "[alt_names]" local i=1 for d in "${dns_arr[@]:-}"; do echo "DNS.$i=$d"; i=$((i+1)); done i=1 for ip in "${ip_arr[@]:-}"; do echo "IP.$i=$ip"; i=$((i+1)); done } > "$EXT_DIR/$name.ext" } gen_key_csr() { local name="$1" openssl genrsa -out "$KEY_DIR/$name.key" 2048 >/dev/null 2>&1 chmod 600 "$KEY_DIR/$name.key" openssl req -new -key "$KEY_DIR/$name.key" \ -out "$CSR_DIR/$name.csr" -subj "/CN=$name" >/dev/null 2>&1 } sign_csr() { local name="$1" days="$2" local serial_flag if [[ -s "$MYCA_DIR/myCA.srl" ]]; then serial_flag=(-CAserial "$MYCA_DIR/myCA.srl") else serial_flag=(-CAcreateserial) fi openssl x509 -req -in "$CSR_DIR/$name.csr" \ -CA "$MYCA_DIR/myCA.pem" -CAkey "$MYCA_DIR/myCA.key" \ "${serial_flag[@]}" -out "$CERT_DIR/$name.crt" -days "$days" -sha256 \ -extfile "$EXT_DIR/$name.ext" >/dev/null 2>&1 chmod 644 "$CERT_DIR/$name.crt" } deploy_nginx() { local name="$1" install -d -m 755 "$NGINX_SSL_DIR" install -m 644 "$CERT_DIR/$name.crt" "$NGINX_SSL_DIR/$name.crt" install -m 600 "$KEY_DIR/$name.key" "$NGINX_SSL_DIR/$name.key" if command -v nginx >/dev/null 2>&1; then nginx -t && systemctl reload nginx || true fi echo "Deployed to $NGINX_SSL_DIR/$name.{crt,key}" } cmd_issue() { local name days=825 reuse_key=0 deploy=0 local dns_args=() ip_args=() shift [[ $# -ge 1 ]] || { usage; exit 2; } name="$1"; shift while [[ $# -gt 0 ]]; do case "$1" in --days) days="$2"; shift 2;; --reuse-key) reuse_key=1; shift;; --deploy-nginx) deploy=1; shift;; --dns) dns_args+=("$2"); shift 2;; --ip) ip_args+=("$2"); shift 2;; *) echo "Unknown option: $1" >&2; exit 2;; esac done ensure_dirs; have_ca make_ext "$name" $(for d in "${dns_args[@]:-}"; do printf -- " --dns %q" "$d"; done) \ $(for i in "${ip_args[@]:-}"; do printf -- " --ip %q" "$i"; done) if [[ $reuse_key -eq 0 || ! -s "$KEY_DIR/$name.key" ]]; then gen_key_csr "$name" else openssl req -new -key "$KEY_DIR/$name.key" \ -out "$CSR_DIR/$name.csr" -subj "/CN=$name" >/dev/null 2>&1 fi sign_csr "$name" "$days" echo "Issued: $CERT_DIR/$name.crt" [[ $deploy -eq 1 ]] && deploy_nginx "$name" } cmd_sign_existing() { local name csr_path="" days=825 local dns_args=() ip_args=() shift [[ $# -ge 1 ]] || { usage; exit 2; } name="$1"; shift while [[ $# -gt 0 ]]; do case "$1" in --csr) csr_path="$2"; shift 2;; --days) days="$2"; shift 2;; --dns) dns_args+=("$2"); shift 2;; --ip) ip_args+=("$2"); shift 2;; *) echo "Unknown option: $1" >&2; exit 2;; esac done [[ -n "$csr_path" && -s "$csr_path" ]] || { echo "CSR not found" >&2; exit 1; } ensure_dirs; have_ca cp -f "$csr_path" "$CSR_DIR/$name.csr" make_ext "$name" $(for d in "${dns_args[@]:-}"; do printf -- " --dns %q" "$d"; done) \ $(for i in "${ip_args[@]:-}"; do printf -- " --ip %q" "$i"; done) sign_csr "$name" "$days" echo "Signed: $CERT_DIR/$name.crt" } cmd_show() { local target="${1:-}" [[ -n "$target" ]] || { usage; exit 2; } [[ "$target" == *.crt || "$target" == *.pem ]] || target="$CERT_DIR/$target.crt" [[ -s "$target" ]] || { echo "Cert not found: $target" >&2; exit 1; } openssl x509 -in "$target" -noout -subject -issuer -dates echo "SANs:" openssl x509 -in "$target" -noout -text | awk '/Subject Alternative Name/{f=1;next}/X509v3/{f=0}f' } cmd_verify() { local target="${1:-}" [[ -n "$target" ]] || { usage; exit 2; } [[ "$target" == *.crt || "$target" == *.pem ]] || target="$CERT_DIR/$target.crt" [[ -s "$target" ]] || { echo "Cert not found: $target" >&2; exit 1; } have_ca openssl verify -CAfile "$MYCA_DIR/myCA.pem" "$target" } cmd_list() { ensure_dirs ls -1 "$CERT_DIR"/*.crt 2>/dev/null || true } cmd_trust_ca() { have_ca install -m 644 "$MYCA_DIR/myCA.pem" /usr/local/share/ca-certificates/myCA.crt update-ca-certificates echo "CA installed to system trust." } main() { [[ $# -ge 1 ]] || { usage; exit 2; } case "$1" in issue) cmd_issue "$@";; sign) cmd_sign_existing "$@";; show) shift; cmd_show "${1:-}";; verify) shift; cmd_verify "${1:-}";; list) cmd_list;; trust-ca) cmd_trust_ca;; -h|--help|help) usage;; *) echo "Unknown command: $1" >&2; usage; exit 2;; esac } main "$@"