Files
edh-keygen/edh-keygen.sh
T
sduesterhaupt 27f665bbf7 feat(edh-keygen): support user and oneshot service actions
Validate configured systemd units before applying service-specific DH
parameter synchronization.

Synchronize per-service DH parameters before restarting active units. Query
system-wide and per-user systemd managers with LoadState, ActiveState,
SubState, and Type.

Restart active units, start inactive or failed Type=oneshot units, and leave
inactive or failed non-oneshot units without a service action. Skip missing
units, unavailable user managers, and invalid or incomplete service
definitions without creating misleading synchronization messages.

Add robust error handling for DH parameter generation, permission changes,
directory creation, global synchronization, and temporary file cleanup.
2026-09-06 14:03:01 +02:00

456 lines
15 KiB
Bash

#!/bin/sh
#
###############################################################################
#
# edh-keygen.sh
#
# Diffie-Hellman Key Generation and Service Management Script
#
# This script generates Diffie-Hellman parameter files for various key sizes,
# manages their permissions and can synchronize keys to custom locations
# with specified ownership and permissions. It supports service restarts
# for both root and non-root systemd users and is designed for integration
# with automated cron jobs.
#
# Configuration is read from a .conf or .local file, supporting per-service
# customization including:
# - Service name and owner
# - DH key size
# - Sync path for DH key
# - User.group for destination
# - File permissions for the key
#
# Authors: Ivo Noack aka Insonic <me@jabber.ivonoack.de>
# Stephan Düsterhaupt <me@jabber.stephanduesterhaupt.de>
#
# Copyright (c) 2016-2026 CB-601 - the open tec Elevator <mail@opensource-technology.de>
# License: MIT
#
# Project Home: https://dev.town-square.de/cb601/edh-keygen
#
###############################################################################
# MIT License
#
# Copyright (c) 2025 CB-601 - the open tec Elevator <mail@opensource-technology.de>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense and/or sell
# copies of the Software and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Default values (in case not set in config)
tls_tmp_path='/etc/pki/tls/tmp'
tls_private_path='/etc/pki/tls/private/'
# Determine config file: prefer edh-keygen.local, else use edh-keygen.conf
script_dir=$(dirname "$0")
if [ -f "$script_dir/edh-keygen.local" ]; then
my_service_conf="$script_dir/edh-keygen.local"
else
my_service_conf="$script_dir/edh-keygen.conf"
fi
# Check config file
if [ ! -f "$my_service_conf" ]; then
echo "Service config file $my_service_conf not found!" 1>&2
exit 1
fi
# Read global settings from config file
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) continue ;; # Skip empty lines and comments
tls_tmp_path=*) tls_tmp_path="${line#tls_tmp_path=}" ;;
tls_private_path=*) tls_private_path="${line#tls_private_path=}" ;;
key_sizes=*) key_sizes="${line#key_sizes=}" ;;
*) break ;; # Stop at first non-global line (service lines start here)
esac
done < "$my_service_conf"
# Create path for 'tmp' in '/etc/pki/tls'
if [ ! -d "$tls_tmp_path" ]; then
if ! mkdir -p "$tls_tmp_path"; then
echo "Failed to create temporary directory $tls_tmp_path." >&2
exit 1
fi
fi
umask 022
# If key_sizes is set in the config, use it; otherwise, extract from service lines or set a default
if [ -z "$key_sizes" ]; then
key_sizes=$(awk -F: 'NF >= 3 && $3 ~ /^[0-9]+$/ { print $3 }' "$my_service_conf" | sort -nu)
[ -z "$key_sizes" ] && key_sizes="2048 4096"
fi
# Generate DH params
for bits in $key_sizes; do
echo "Generating DH parameters for $bits bits..."
if ! openssl dhparam -out "$tls_tmp_path/dh_${bits}.pem" "$bits"; then
echo "Failed to generate ${bits}-bit DH parameters." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
done
# Set permissions
if ! find "$tls_tmp_path" -type f -name '*.pem' -exec chmod 644 {} \;; then
echo "Failed to set permissions on generated DH parameters." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
# Synchronize DH parameters from the temporary to the private directory.
if [ ! -d "$tls_private_path" ]; then
if ! mkdir -p "$tls_private_path"; then
echo "Failed to create private key directory $tls_private_path." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
fi
if ! chown root:root "$tls_private_path"; then
echo "Failed to set ownership on private key directory $tls_private_path." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
if ! chmod 750 "$tls_private_path"; then
echo "Failed to set permissions on private key directory $tls_private_path." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
for dh_file in "$tls_tmp_path"/dh_*.pem; do
if [ ! -f "$dh_file" ]; then
echo "No generated DH parameter files found in $tls_tmp_path." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
if ! rsync -a "$dh_file" "$tls_private_path"; then
echo "Failed to synchronize $dh_file to $tls_private_path." >&2
rm -rf "$tls_tmp_path"
exit 1
fi
done
# Delete the temporary files
if ! rm -rf "$tls_tmp_path"; then
echo "Failed to remove temporary directory $tls_tmp_path." >&2
exit 1
fi
# Query systemd unit properties for system-wide and per-user service units.
#
# get_service_property <property> <service> <owner> <uid>
#
# Prints the value of the requested systemd property to standard output.
# Root-owned services are queried through the system manager. Services owned
# by another user are queried through that user's systemd --user manager.
# The caller must provide the numeric UID for per-user units so that the
# required XDG_RUNTIME_DIR can be set explicitly.
#
# get_service_state <service> <owner>
#
# Resolves and validates the systemd state required for service handling.
# On success, it sets the following global variables for the current service:
#
# load_state Unit load state, e.g. loaded or not-found.
# active_state High-level unit state, e.g. active, inactive, or failed.
# sub_state Unit type-specific detailed state, e.g. running, dead,
# exited, or failed.
# service_type Service type, e.g. simple, notify, forking, or oneshot.
# service_uid Numeric UID for per-user units; empty for root units.
#
# A LoadState of "not-found" is returned as a successful lookup so the caller
# can report a missing configured unit explicitly. Other query failures return
# a non-zero status.
get_service_property() {
edh_property_name=$1
edh_service_name=$2
edh_service_owner=$3
edh_service_uid=$4
if [ "$edh_service_owner" = "root" ]; then
/usr/bin/systemctl show \
--property="$edh_property_name" \
--value \
"$edh_service_name.service" 2>/dev/null
else
sudo -u "$edh_service_owner" \
XDG_RUNTIME_DIR="/run/user/$edh_service_uid" \
/usr/bin/systemctl --user show \
--property="$edh_property_name" \
--value \
"$edh_service_name.service" 2>/dev/null
fi
}
# Return 0 for a valid unit state or for LoadState=not-found.
# Return non-zero if the owner does not exist or systemd cannot be queried.
# Result values are exported through the global variables documented above.
get_service_state() {
edh_state_service=$1
edh_state_owner=$2
load_state=
active_state=
sub_state=
service_type=
service_uid=
if [ "$edh_state_owner" != "root" ]; then
service_uid=$(id -u "$edh_state_owner" 2>/dev/null)
if [ -z "$service_uid" ]; then
return 1
fi
fi
load_state=$(get_service_property \
LoadState \
"$edh_state_service" \
"$edh_state_owner" \
"$service_uid")
if [ -z "$load_state" ]; then
return 1
fi
if [ "$load_state" = "not-found" ]; then
return 0
fi
active_state=$(get_service_property \
ActiveState \
"$edh_state_service" \
"$edh_state_owner" \
"$service_uid") || return 1
sub_state=$(get_service_property \
SubState \
"$edh_state_service" \
"$edh_state_owner" \
"$service_uid") || return 1
service_type=$(get_service_property \
Type \
"$edh_state_service" \
"$edh_state_owner" \
"$service_uid") || return 1
[ -n "$active_state" ] &&
[ -n "$sub_state" ] &&
[ -n "$service_type" ]
}
# Read and process service list
while IFS= read -r line || [ -n "$line" ]; do
# Skip empty lines, comments and global settings (lines with '=')
case "$line" in
''|\#*|*=*) continue ;;
esac
# Extract service, owner and sync parameters
service=$(printf '%s\n' "$line" | awk -F: '{print $1}')
owner=$(printf '%s\n' "$line" | awk -F: '{print $2}')
key_size=$(printf '%s\n' "$line" | awk -F: '{print $3}')
sync_path=$(printf '%s\n' "$line" | awk -F: '{print $4}')
user_group=$(printf '%s\n' "$line" | awk -F: '{print $5}')
permissions=$(printf '%s\n' "$line" | awk -F: '{print $6}')
# Validate required base fields.
if [ -z "$service" ] || [ -z "$owner" ]; then
echo "Invalid service definition: $line" >&2
continue
fi
echo "$service.service (owner: $owner)..."
if ! get_service_state "$service" "$owner"; then
if [ "$owner" != "root" ] && [ -z "$service_uid" ]; then
echo "User $owner not found. Skipping $service." >&2
else
echo "Could not query the systemd state of $service.service. Skipping restart and DH key sync." >&2
fi
continue
fi
if [ "$load_state" = "not-found" ]; then
if [ "$owner" = "root" ]; then
echo "$service.service does not exist. Skipping restart and DH key sync." >&2
else
echo "$service.service does not exist for user $owner. Skipping restart and DH key sync." >&2
fi
continue
fi
# Decide which action to apply after a successful optional DH key sync.
service_action=none
case "$active_state" in
active)
service_action=restart
;;
inactive)
if [ "$service_type" = "oneshot" ]; then
service_action=start
echo "$service.service is inactive (SubState: $sub_state, Type: oneshot). Syncing DH key and starting unit."
else
echo "$service.service is inactive (SubState: $sub_state). DH parameters are updated; skipping service action."
fi
;;
failed)
if [ "$service_type" = "oneshot" ]; then
service_action=start
echo "$service.service is failed (SubState: $sub_state, Type: oneshot). Syncing DH key and starting unit." >&2
else
echo "$service.service is failed (SubState: $sub_state). Syncing DH key without restart." >&2
fi
;;
*)
echo "$service.service is $active_state (SubState: $sub_state). Skipping restart and DH key sync." >&2
continue
;;
esac
# Synchronize DH parameters when a complete extended configuration is present.
if [ -z "$key_size" ] && [ -z "$sync_path" ] && [ -z "$user_group" ] && [ -z "$permissions" ]; then
:
elif [ -z "$key_size" ] || [ -z "$sync_path" ] || [ -z "$user_group" ] || [ -z "$permissions" ]; then
echo "Incomplete DH key sync configuration for $service. Skipping sync and service action." >&2
continue
else
# The configuration format uses user.group. Convert it to user:group for chown.
case "$user_group" in
*.*)
ug_user=${user_group%%.*}
ug_group=${user_group#*.}
;;
*)
echo "Invalid owner '$user_group' for $service; expected user.group. Skipping sync." >&2
continue
;;
esac
if [ -z "$ug_user" ] || [ -z "$ug_group" ]; then
echo "Invalid owner '$user_group' for $service; expected user.group. Skipping sync." >&2
continue
fi
dh_file="${tls_private_path%/}/dh_${key_size}.pem"
target_file="${sync_path%/}/dh_${key_size}.pem"
if [ ! -f "$dh_file" ]; then
echo "DH key $dh_file not found. Skipping sync for $service." >&2
continue
fi
# Create the destination directory if it does not exist.
if [ ! -d "$sync_path" ]; then
if ! mkdir -p "$sync_path"; then
echo "Failed to create $sync_path for $service." >&2
continue
fi
if ! chown "$ug_user:$ug_group" "$sync_path"; then
echo "Failed to set ownership on $sync_path for $service." >&2
continue
fi
if ! chmod 750 "$sync_path"; then
echo "Failed to set permissions on $sync_path for $service." >&2
continue
fi
echo "Created directory $sync_path for $service."
fi
if ! cp "$dh_file" "$target_file"; then
echo "Failed to copy DH key to $target_file for $service." >&2
continue
fi
if ! chown "$ug_user:$ug_group" "$target_file"; then
echo "Failed to set ownership on $target_file for $service." >&2
continue
fi
if ! chmod "$permissions" "$target_file"; then
echo "Failed to set permissions on $target_file for $service." >&2
continue
fi
echo "Synced DH key (${key_size}-bit) to $sync_path for $service."
fi
# Apply the selected action after the optional DH key synchronization.
case "$service_action" in
restart)
echo "$service.service is active (SubState: $sub_state), restarting as $owner..."
if [ "$owner" = "root" ]; then
if ! /usr/bin/systemctl restart "$service.service"; then
echo "Failed to restart $service.service." >&2
continue
fi
else
if ! sudo -u "$owner" \
XDG_RUNTIME_DIR="/run/user/$service_uid" \
/usr/bin/systemctl --user restart "$service.service"; then
echo "Failed to restart $service.service as $owner." >&2
continue
fi
fi
echo "$service.service restarted."
;;
start)
echo "$service.service is $active_state oneshot, starting as $owner..."
if [ "$owner" = "root" ]; then
if ! /usr/bin/systemctl start "$service.service"; then
echo "Failed to start $service.service." >&2
continue
fi
else
if ! sudo -u "$owner" \
XDG_RUNTIME_DIR="/run/user/$service_uid" \
/usr/bin/systemctl --user start "$service.service"; then
echo "Failed to start $service.service as $owner." >&2
continue
fi
fi
echo "$service.service started."
;;
none)
:
;;
*)
echo "Internal error: unsupported action '$service_action' for $service." >&2
continue
;;
esac
done < "$my_service_conf"
exit 0