Deploying custom CA certificates on Linux from Windows share
This post is about deploying custom certificate authority (CA) certificates onto Linux hosts, from an anonymous Windows share, then deploying them to be used by web-browsers (which seem to use their own CA stores these days). There are two scripts, one for each of these tasks, as installing to the system store usually requires super-user (i.e. root
) access but installing to the browser stores is per-user (and should not be done using the super-user account).
Installing CA to the system store
This script does make several assumptions and, at present, needs to be configured using in-line variables CERT_DIR
, CERT_PREFIX
and DEBUG
- these should be made command-line options.
Key assumptions are:
- Certificates are all in the same directory, directly specified as the download source.
- There is no need to remove previously installed certificates that are no longer in the share.
- The share allows anonymous access to fetch the certificates.
#!/bin/bash
# Standard bash safety - fail on error, do not allow unintended
# globbing, do not allow uninitialised variables, errors in pipes
# cause whole pipe to fail.
set -efuo pipefail
# TODO: Make these command line options
# UNC path to fetch the certificates from - should be a directory
# containing one or more certifictes to be installed.
CERT_DIR="//isolinear/software/certs/my super ca"
# This will be used as a directory (Debian-like) or filename prefix
# (Red Hat-like) when installing the certificates.
CERT_PREFIX="LOCAL-SUPER-CA"
DEBUG=true
debug() {
[[ $DEBUG = true ]] && echo "$@" >&2
}
if [[ $UID -eq 0 ]]
then
USE_SUDO=false
elif sudo -n -l &>/dev/null
then
# N.B. being able to list sudo just means we can run sudo
# passwordless (e.g. already authenticated or have nopasswd
# commands available). It does not mean we can run the specific
# commands required later - the script may still error.
USE_SUDO=true
else
echo "This script needs to be run as root, or be able to sudo," \
"to install the certificates." >&2
exit 1
fi
do_su() {
if [[ ${USE_SUDO} = "true" ]]
then
sudo "$@"
else
"$@"
fi
}
echo "Fetching certificates from ${CERT_DIR}..."
# Split into //host/share and directory
regex='^(//[^/]+/[^/]+)(/.*)?$'
if [[ ${CERT_DIR} =~ $regex ]]
then
# First group
host_share="${BASH_REMATCH[1]}"
# Is there a 2nd group, or is it the empty string (didn't match)?
if [[ -z "${BASH_REMATCH[2]}" ]]
then
# Presume root of share, if no directory
share_dir='/'
else
share_dir="${BASH_REMATCH[2]}"
fi
debug "Calculated host and share name as: ${host_share}"
debug "Calculated directory as: ${share_dir}"
# Work out how many slashes are in the directory name - we will
# need to strip these off.
dir_components="${share_dir//[^\/]}"
dir_component_count=${#dir_components}
debug "Will strip ${dir_component_count}+1 components."
# Somewhere to save the files. Make sure this gets tidied up
# (deleted) later on.
download_dir=$( mktemp -d )
download_tar=$( mktemp )
# Get smbclient to tar the directory
# This was originally done without a temporary tar file, directly piping
# smbclient to tar but tar doesn't handle SIGPIPE correctly so that caused
# a failure with '-o pipefail'.
# See https://stackoverflow.com/questions/769564/error-code-141-with-tar
debug "Downloading files to tar file at ${download_tar}..."
smbclient "${host_share}" -N -d0 -D "${share_dir}" -Tc "${download_tar}"
debug "Extracting downloaded files into ${download_dir}..."
# Extract into the download_dir, stripping off the directory name.
tar -xC "${download_dir}" -f "${download_tar}" \
--strip-components=$(( ${dir_component_count} + 1 ))
# Actually install the certificates - where they go and what command to
# use to update the system store depends on if this is a Debain or Red
# Hat like distribution (others are available, only support those two
# currently).
if [ -f /etc/debian_version ]
then
debug "Debian-like distribution, installing to" \
"/usr/local/share/ca-certificates and using update-ca-certificates"
target_dir="/usr/local/share/ca-certificates/${CERT_PREFIX}"
# Check the install directory exists
if ! [ -e "${target_dir}" ]
then
debug "Creating target ${target_dir}..."
# /usr/local/share/ca-certificates should already exist (and be
# empty on a new install) so '-p' is unnecessary.
do_su mkdir "${target_dir}"
# Certificate files should be public
do_su chmod 755 "${target_dir}"
else
# Don't currently handle deleting old/defunct certificate files
debug "${target_dir} already exists - not cleaning out any old files."
fi
# Install the certificates
set +f # Need to glob
for file in "${download_dir}"/*
do
set -f # Done globbing
# Only copy plain files - this presumes all certificates are in
# the top level directory but saves us from worrying about
# clashing file names in different directories in the Windows share.
if [ -f "${file}" ]
then
debug "Installing $( basename "${file}" )..."
# Does the source file end with '.crt' (required for
# update-ca-certificates)? If not, add it.
if [[ "${file}" = "*.crt" ]]
then
target_file="$( basename "${file}" )"
else
# Strip existing extension
target_file="$( basename "${file%.*}" ).crt"
fi
debug "Will copy to destination filename ${target_file}."
# Check permissions are what we want on source
do_su chmod 444 "${file}"
# Copy, preserving permissions and timestamps (default also
# preserves owner but I _want_ it to be root).
do_su cp --preserve=mode,timestamps "${file}" \
"${target_dir}/${target_file}"
fi
done
# In case the loop didn't get entered
set -f # Done globbing
# Update the system certificates
debug "Running update-ca-certificates to add new certs"
do_su update-ca-certificates
elif [ -f /etc/redhat-release ]
then
debug "Red Hat-like distribution, installing to" \
"/etc/pki/ca-trust/source/anchors and using update-ca-trust"
# Install the certificates
set +f # Need to glob
for file in "${download_dir}"/*
do
set -f # Done globbing
# Only copy plain files - this presumes all certificates are in
# the top level directory but saves us from worrying about
# clashing file names in different directories in the Windows share.
if [ -f "${file}" ]
then
debug "Installing $( basename "${file}" )..."
# Check permissions are what we want on source
do_su chmod 444 "${file}"
target="/etc/pki/ca-trust/source/anchors/${CERT_PREFIX}-$( basename "${file}" )"
[ -e "${target}" ] && echo "WARNING: Overwriting ${target}" >&2
# Copy, preserving permissions and timestamps (default also
# preserves owner but I _want_ it to be root).
do_su cp -f --preserve=mode,timestamps "${file}" "${target}"
fi
done
# In case the loop didn't get entered
set -f # Done globbing
# Update the system certificates
debug "Running update-ca-trust to add new certs"
do_su update-ca-trust
else
echo "Unknown distribution - unable to install certificates." >&2
exit_state=1
fi
# Tidy up
debug "Deleting ${download_dir}"
rm -rf "${download_dir}"
debug "Deleting ${download_tar}"
rm -f "${download_tar}"
else
echo "Unable to separate path into //host/share and directory parts." >&2
exit 1
fi
# Exit with exit_state or zero (0) if none given.
exit ${exit_state:-0}
Adding custom certificates to a user’s web-browser store.
#!/bin/bash
# Standard bash safety - fail on error, do not allow unintended
# globbing, do not allow uninitialised variables, errors in pipes
# cause whole pipe to fail.
set -efuo pipefail
# This will be used to identify the certificates to add, from the
# system store.
CERT_PREFIX="LOCAL-SUPER-CA"
DEBUG=true
debug() {
[[ $DEBUG = true ]] && echo "$@" >&2
}
if [[ $UID -eq 0 ]]
then
# Not to mention the question "why are you running a web-browser as root?"
echo "This script should not be run as root - adding certificates" \
"to root's web browser is a bad idea." >&2
exit 1
fi
if ! command -v certutil &>/dev/null
then
echo "certutil command not found - perhaps it needs installing" \
"(libnss3-tools on Debian)?" >&2
exit 1
fi
# Based on https://superuser.com/a/1717924
add_certificate() {
debug "Adding $1 to older browsers..."
for cert_db in $(find ~ -name cert8.db)
do
debug "Adding to ${cert_db}..."
# Use base filename sans extension as name
certutil -A -n "$( basename "${1%.*}" )" -t "TCu,Cu,Tc" -i "$1" \
-d "dbm:$( dirname "${cert_db}" )"
done
debug "Adding $1 to newer browsers..."
for cert_db in $(find ~ -name cert9.db)
do
debug "Adding to ${cert_db}..."
# Use base filename sans extension as name
certutil -A -n "$( basename "${1%.*}" )" -t "TCu,Cu,Tc" -i "$1" \
-d "sql:$( dirname "${cert_db}" )"
done
}
# Like installing the certificates, where they are depends on the OS
if [ -f /etc/debian_version ]
then
set +f # Enable globbing
shopt -s nullglob # No matching files is fine
for file in "/usr/local/share/ca-certificates/${CERT_PREFIX}"/*
do
set -f # Done globbing
shopt -u nullglob # Restore default (no null globbing)
debug "Adding file ${file}..."
add_certificate "${file}"
done
# These are in case the loop didn't get entered
set -f # Done globbing
shopt -u nullglob # Restore default (no null globbing)
elif [ -f /etc/redhat-release ]
then
set +f # Enable globbing
shopt -s nullglob # No matching files is fine
for file in "/etc/pki/ca-trust/source/anchors/${CERT_PREFIX}-"*
do
set -f # Done globbing
shopt -u nullglob # Restore default (no null globbing)
debug "Adding file ${file}..."
add_certificate "${file}"
done
# These are in case the loop didn't get entered
set -f # Done globbing
shopt -u nullglob # Restore default (no null globbing)
else
echo "Unknown distribution - unable to install certificates." >&2
exit 1
fi