Last active
May 23, 2025 15:59
-
-
Save nicholaswmin/8cd00c9661a0c3c29e5e86859fb6baa0 to your computer and use it in GitHub Desktop.
Creates and automatically trusts self-signed SSL on macOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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