Skip to content

Instantly share code, notes, and snippets.

@nicholaswmin
Last active May 23, 2025 15:59
Show Gist options
  • Save nicholaswmin/8cd00c9661a0c3c29e5e86859fb6baa0 to your computer and use it in GitHub Desktop.
Save nicholaswmin/8cd00c9661a0c3c29e5e86859fb6baa0 to your computer and use it in GitHub Desktop.
Creates and automatically trusts self-signed SSL on macOS
#!/usr/bin/env zsh
autoload -U colors && colors
# Check for correct shell
if [ -z "$ZSH_VERSION" ]; then
echo "Error: This script must be run with zsh, not sh or bash"
echo "Please run with: zsh ./vouch.zsh install"
exit 1
fi
# vouch.zsh
# > create and automatically trust local SSL
# > supports macOS:
# > - Apple Silicon (M+) # tested on Sequioa
# > - Intel # untested, possibly OK
#
# usage:
# vouch.zsh install # install local ssl
# vouch.zsh -h | --help # print this message
#
# env.vars:
# DIRNAME="dev" # directory for certificate files
# DOMAIN="localhost" # hostname for SSL certificate
# CERT_DAYS="3650" # certificate validity
# # mkcert ignores, ~10yr default
# FORCE_COLOR="1" # force colored output always
# NO_COLOR="1" # disable colored output always
#
# use case:
# - your dev server must serve HTTPS.
# or you get browser insecure warning
# - you don't have local SSL,
# or they have expired
#
# user flow:
# - you run this
# - creates a server.pem + server.key
# - adds them on your Keychain so browsers trust them
# - you must serve them via your dev server in some way
# - visiting your https://localhost:**** works w/o warnings
#
# © 2025 - nicholaswmin - MIT License
# config
# ----
DIRNAME=${DIRNAME:-"dev"}
DOMAIN=${DOMAIN:-"localhost"}
CERT_DAYS=${CERT_DAYS:-"3650"}
CERT_FILE="${DIRNAME}/server.pem"
KEY_FILE="${DIRNAME}/server.key"
# Detect Rosetta 2 on Apple Silicon
is_rosetta() {
[[ "$(uname -m)" == "x86_64" ]] && [[ "$(sysctl -n machdep.cpu.brand_string)" == *"Apple"* ]]
}
# Color function respecting NO_COLOR and FORCE_COLOR standards
col() {
local color="$1"
local text="$2"
# Disable color if NO_COLOR is set and non-empty
if [[ -n "${NO_COLOR}" ]]; then
printf "%s" "$text"
return 0
fi
# Disable color if not interactive AND not forced
if [[ -z "${FORCE_COLOR}" && ! -t 1 && ! -t 2 ]]; then
printf "%s" "$text"
return 0
fi
# Use colors (default or forced)
case "$color" in
red) printf "${fg[red]}%s${reset_color}" "$text" ;;
cyan) printf "${fg[cyan]}%s${reset_color}" "$text" ;;
green) printf "${fg[green]}%s${reset_color}" "$text" ;;
yellow) printf "${fg[yellow]}%s${reset_color}" "$text" ;;
default) printf "${fg[default]}%s${reset_color}" "$text" ;;
dim) printf "\033[2m%s\033[0m" "$text" ;;
*) printf "%s" "$text" ;;
esac
}
# utilities
# ----
log() { [[ $# -gt 0 ]] && printf "$(col dim "􀅽 %s")\n" "$@"; }
log_error() { printf "$(col red "􀆄 %s")\n" "$1"; shift; log "$@"; }
log_info() { printf "$(col cyan "􀆅 %s")\n" "$1"; }
log_done() { printf "$(col green "􁓵 %s")\n" "$1"; shift; log "$@"; }
log_warn() { printf "$(col yellow "􁑣 %s")\n" "$1"; }
log_spacer() { echo; }
# Show help message
show_help() {
printf "\n"
printf " $(col cyan "􀇺 vouch.zsh")\n"
printf " $(col yellow "create and trust local SSL on macOS")\n"
printf "\n"
printf " $(col default "usage:")\n"
printf " $(col dim " vouch.zsh install # install local ssl")\n"
printf "$(col dim " vouch.zsh -h | --help # print this message")\n"
printf "\n"
printf " $(col default "env. vars:")\n"
printf " $(col dim " DIRNAME=\"dev\" # directory for certificate files")\n"
printf "$(col dim " DOMAIN=\"localhost\" # hostname for SSL certificate")\n"
printf "$(col dim " CERT_DAYS=\"3650\" # certificate validity,")\n"
printf "$(col dim " mkcert ignores, ~10yr default")\n"
printf "$(col dim " FORCE_COLOR=\"1\" # force colored output always")\n"
printf "$(col dim " NO_COLOR=\"1\" # disable colored output always")\n"
printf "\n"
printf " $(col default "use case:")\n"
printf " $(col dim "- your dev server must serve HTTPS.")\n"
printf "$(col dim " or you get browser insecure warning")\n"
printf " $(col dim "- you don't have local SSL,")\n"
printf "$(col dim " or they have expired")\n"
printf "\n"
printf " $(col default "user flow:")\n"
printf " $(col dim "- you run this")\n"
printf " $(col dim "- creates a server.pem + server.key")\n"
printf " $(col dim "- adds them on your Keychain so browsers trust them")\n"
printf " $(col dim "- you must serve them via your dev server in some way")\n"
printf " $(col dim "- visiting your https://localhost:**** works w/o warnings")\n"
printf "\n"
printf " $(col dim "© 2025 - nicholaswmin - MIT License")\n"
printf " \n"
exit 0
}
# Check if mkcert is installed
check_mkcert() {
command -v mkcert &> /dev/null
}
# Install mkcert via Homebrew
install_mkcert() {
command -v brew &> /dev/null || return 1
if is_rosetta; then
arch -arm64 brew install mkcert
else
brew install mkcert
fi
return $?
}
# Create and install local Certificate Authority
create_ca() {
mkcert -install >/dev/null 2>&1
}
# Generate SSL certificate files
# usage: create_cert <keyfile> <certfile> <domain>
create_cert() {
local output=$(mkcert -key-file "$1" -cert-file "$2" "$3" 2>&1)
local exit_code=$?
if [[ $exit_code -eq 0 ]]; then
log_info "Created certificate for $3"
echo "$output" | grep -E '(certificate is at|It will expire)' | \
sed 's/^/ /' | log
fi
return $exit_code
}
# Install SSL certificates command
install_command() {
log_spacer
log "Setting up local SSL for https://${DOMAIN}:xxxx"
# Check and install mkcert
if ! check_mkcert; then
! command -v brew &> /dev/null && \
log_error "Homebrew not found" \
"Run: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" \
"Then: source ~/.zshrc" && exit 1
log_spacer
log "Installing mkcert via Homebrew..."
is_rosetta && log_warn "Using ARM64 prefix for Rosetta 2 compatibility"
if ! install_mkcert; then
log_error "Failed to install mkcert" \
"Check your Homebrew installation" \
"Try: brew update && brew install mkcert" && exit 1
fi
fi
# Create local CA
log_spacer
log "Creating local Certificate Authority..."
create_ca && log_info "Installed local Certificate Authority in keychain" || {
log_error "Failed to install local CA" \
"Check macOS security settings" \
"Try running with sudo if needed"
exit 1
}
# Prepare certificate directory
log_spacer
log "Preparing certificate directory..."
[[ -d "${DIRNAME}" ]] && log_warn "Directory '${DIRNAME}' exists. Will overwrite certificates"
mkdir -p "${DIRNAME}"
# Generate certificate
log_spacer
log "Generating certificate for ${DOMAIN}..."
[[ -f "${KEY_FILE}" || -f "${CERT_FILE}" ]] && log_warn "Certificate files exist. Overwriting"
create_cert "${KEY_FILE}" "${CERT_FILE}" "${DOMAIN}" || {
log_error "Failed to generate certificates" \
"Check write permissions in current directory" \
"Ensure mkcert is working: mkcert -version"
exit 1
}
log_spacer
log_done "Local SSL setup complete!" \
"Chrome & Safari now trust https://${DOMAIN} on any port" \
"Ensure your server serves ${KEY_FILE} and ${CERT_FILE}"
log_spacer
}
# Show error for missing command and display help
missing_command() {
log_error "must choose a command"
echo
print -P "$(col dim "$(show_help | cat)")"
exit 1
}
# Command dispatcher
main() {
case "$1" in
install)
install_command
;;
-h|--help)
show_help
;;
*)
[[ -z "$1" ]] && missing_command || {
log_error "unknown command '$1'"
echo
exit 1
}
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment