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.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.0.0 - 2026-09-06
|
||||
|
||||
### Added
|
||||
|
||||
- Added configurable Diffie-Hellman parameter generation for one or more key sizes.
|
||||
- Added global configuration options for temporary and persistent DH parameter directories.
|
||||
- Added support for simple service definitions using the `service:owner` format.
|
||||
- Added extended service definitions for per-service DH parameter deployment, destination ownership, and file permissions.
|
||||
- Added support for both system-wide systemd services and per-user `systemctl --user` services.
|
||||
- Added support for rootless container service units by executing user service actions with the configured service owner and `XDG_RUNTIME_DIR`.
|
||||
- Added systemd state inspection through `LoadState`, `ActiveState`, `SubState`, and `Type`.
|
||||
- Added automatic restart of active units after a successful optional service-specific DH parameter synchronization.
|
||||
- Added automatic execution of inactive or failed `Type=oneshot` units after a successful DH parameter update.
|
||||
- Added explicit validation and error reporting for missing users, missing units, unavailable user managers, incomplete service definitions, missing DH files, and failed file operations.
|
||||
- Added secure, explicit ownership and permission handling for created target directories and deployed DH parameter files.
|
||||
- Added cleanup and failure handling for temporary DH parameter files and the global synchronization step.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed service handling so that service-specific DH parameters are deployed before a service is restarted or a one-shot unit is started.
|
||||
- Changed unit validation to distinguish missing services from inactive, failed, active, and transitional unit states.
|
||||
- Changed reporting for per-user services to show the configured owner consistently for both status checks and service actions.
|
||||
- Changed configuration parsing to preserve compatibility with `user.group` ownership syntax while converting it to `user:group` for `chown`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed misleading success messages for services that do not exist on a host.
|
||||
- Fixed the possibility of creating or synchronizing DH parameters to a configured target path when the corresponding systemd unit is missing.
|
||||
- Fixed malformed error-handling blocks around destination-directory creation and DH parameter copy operations.
|
||||
- Fixed validation of the configured `user.group` ownership field.
|
||||
- Fixed user-service restart handling so that active `systemctl --user` services are restarted as their configured owner.
|
||||
- Fixed handling of active `Type=oneshot` units with `SubState=exited` by using `ActiveState` for action decisions.
|
||||
+316
-55
@@ -84,7 +84,10 @@ done < "$my_service_conf"
|
||||
|
||||
# Create path for 'tmp' in '/etc/pki/tls'
|
||||
if [ ! -d "$tls_tmp_path" ]; then
|
||||
mkdir -p "$tls_tmp_path"
|
||||
if ! mkdir -p "$tls_tmp_path"; then
|
||||
echo "Failed to create temporary directory $tls_tmp_path." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
umask 022
|
||||
@@ -98,17 +101,165 @@ fi
|
||||
# Generate DH params
|
||||
for bits in $key_sizes; do
|
||||
echo "Generating DH parameters for $bits bits..."
|
||||
openssl dhparam -out "$tls_tmp_path/dh_${bits}.pem" "$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
|
||||
find "$tls_tmp_path" -type f -name "*.pem" -exec chmod 644 {} \;
|
||||
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
|
||||
|
||||
# Sync certs from temporary to private folder
|
||||
rsync -a "$tls_tmp_path/"*.pem "$tls_private_path"
|
||||
# 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
|
||||
rm -rf "$tls_tmp_path"
|
||||
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
|
||||
@@ -118,76 +269,186 @@ while IFS= read -r line || [ -n "$line" ]; do
|
||||
esac
|
||||
|
||||
# Extract service, owner and sync parameters
|
||||
service=$(printf "%s" "$line" | awk -F: '{print $1}')
|
||||
owner=$(printf "%s" "$line" | awk -F: '{print $2}')
|
||||
key_size=$(printf "%s" "$line" | awk -F: '{print $3}')
|
||||
sync_path=$(printf "%s" "$line" | awk -F: '{print $4}')
|
||||
user_group=$(printf "%s" "$line" | awk -F: '{print $5}')
|
||||
permissions=$(printf "%s" "$line" | awk -F: '{print $6}')
|
||||
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}')
|
||||
|
||||
# Convert legacy user.group notation to user:group
|
||||
ug_user=${user_group%%.*}
|
||||
ug_group=${user_group#*.}
|
||||
user_group=$ug_user:$ug_group
|
||||
# Validate required base fields.
|
||||
if [ -z "$service" ] || [ -z "$owner" ]; then
|
||||
echo "Invalid service definition: $line" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$service.service (owner: $owner)..."
|
||||
|
||||
# Check service status (must run as root)
|
||||
if [ "$owner" = "root" ]; then
|
||||
mySubState=$(systemctl show -p SubState --value "$service.service" 2>/dev/null)
|
||||
if ! get_service_state "$service" "$owner"; then
|
||||
if [ "$owner" != "root" ] && [ -z "$service_uid" ]; then
|
||||
echo "User $owner not found. Skipping $service." >&2
|
||||
else
|
||||
uid=$(id -u "$owner" 2>/dev/null)
|
||||
if [ -n "$uid" ]; then
|
||||
mySubState=$(sudo -u "$owner" XDG_RUNTIME_DIR="/run/user/$uid" /usr/bin/systemctl --user show -p SubState --value "$service.service" 2>/dev/null)
|
||||
else
|
||||
echo "User $owner not found! Skipping $service." 1>&2
|
||||
continue
|
||||
echo "Could not query the systemd state of $service.service. Skipping restart and DH key sync." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$mySubState" = "running" ]; then
|
||||
echo "$service.service is running, restarting as $owner..."
|
||||
if [ "$owner" = "root" ]; then
|
||||
/usr/bin/systemctl restart "$service.service"
|
||||
else
|
||||
sudo -u "$owner" XDG_RUNTIME_DIR="/run/user/$uid" /usr/bin/systemctl --user restart "$service"
|
||||
fi
|
||||
echo "$service.service restarted."
|
||||
fi
|
||||
|
||||
# Handle DH key sync if parameters exist
|
||||
if [ -n "$key_size" ] && [ -n "$sync_path" ] && [ -n "$user_group" ] && [ -n "$permissions" ]; then
|
||||
if ! echo "$user_group" | grep -q "."; then
|
||||
echo "Error: user_group must be 'user.group' for $service. Skipping sync." 1>&2
|
||||
continue
|
||||
fi
|
||||
|
||||
dh_file="$tls_private_path/dh_${key_size}.pem"
|
||||
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." 1>&2
|
||||
echo "DH key $dh_file not found. Skipping sync for $service." >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create directory if missing
|
||||
# Create the destination directory if it does not exist.
|
||||
if [ ! -d "$sync_path" ]; then
|
||||
mkdir -p "$sync_path" || {
|
||||
echo "Failed to create $sync_path for $service." 1>&2
|
||||
if ! mkdir -p "$sync_path"; then
|
||||
echo "Failed to create $sync_path for $service." >&2
|
||||
continue
|
||||
}
|
||||
chown "$user_group" "$sync_path"
|
||||
chmod 750 "$sync_path"
|
||||
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
|
||||
|
||||
# Copy DH key and set permissions
|
||||
cp "$dh_file" "$sync_path/" || {
|
||||
echo "Failed to copy DH key to $sync_path for $service." 1>&2
|
||||
if ! cp "$dh_file" "$target_file"; then
|
||||
echo "Failed to copy DH key to $target_file for $service." >&2
|
||||
continue
|
||||
}
|
||||
chown "$user_group" "$sync_path/dh_${key_size}.pem"
|
||||
chmod "$permissions" "$sync_path/dh_${key_size}.pem"
|
||||
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"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user