agama/live/live-root/usr/bin/agama-issue-generator
Ladislav Slezák 8ff52c8717
Use inst.remote=0 for disabling the remote access to Agama (#3336)
## Problem

- Using the new `inst.listen_on` boot option to disable remote access to
the server looks inconsistent (for disabling the SSH access you can use
the `systemd.mask=sshd.service` boot option)
- Simplify the implementation, limiting access to some interface or IP
address was not requested
- Related to https://github.com/agama-project/agama/pull/3269

## Solution

- Use systemd services to implement disabling the remote access
- ~~Implement new `agama-web-server-local` and `agama-web-server-remote`
services~~
- ~~By default the remote service is enabled, but the user can disable
it and enable the local service instead using the
`systemd.mask=agama-web-server-remote.service
systemd.wants=agama-web-server-local.service` boot parameters.~~
- Support new `inst.remote=0` boot option

## Testing

- Tested manually

## Notes

I tried several approaches but either they did not work or were a bit
hacky:

1. First a wanted to be the both local and remote services enabled by
default. Because of conflicts only one of them should be running at a
time. I used `Before` and `After` so the remote service starts first and
the conflict should block the local service from running. Disabling the
remote service allows running the local service automatically.
The problem was that there is probably some race condition and sometimes
the local service was actually running instead of the remote by default.
I tried several changes but nothing helped.
2. Then I added code to the local service to explicitly check whether
the remote service is disabled or not (using `ExecCondition` attribute).
But that feels a bit hacky and it is a hidden magic.
3. Then I realized that the same way how you can disable a service at
boot using `systemd.mask` option you can also enable a disabled service
using the `systemd.wants` parameter. So instead of enabling both
services enable only the default remote one. The users then can
explicitly disable the remote service and enable the local one using the
`systemd.mask=agama-web-server-remote.service
systemd.wants=agama-web-server-local.service` boot options. But that
turned out to be quite complicated.
4. So let's implement simple `inst.remote=0` boot option which disables
the remotes access to the Agama installer. The SSH service can be
disabled using the standard `systemd.mask=sshd.service` option.
2026-04-13 17:10:54 +02:00

352 lines
13 KiB
Bash
Executable file

#!/bin/bash
# This is a helper script which generates issue file displayed at the console
# before logging in. Partly inspired by the issue-generator package
# (https://github.com/thkukuk/issue-generator).
#
# Generates several issue files:
# - Welcome message with Agama version number (--welcome option)
# - Agama SSL certificate fingerprints (--ssl option)
# - SSH host key fingerprints (--ssh option)
# - Agama access URL for all network devices (--watch-network option)
# NOTE: in this case the script does not finish, it watches the changes in
# the NetworkManager configuration and updates the URL if needed
# - Agama access URL using the mDNS (Avahi) URL (--watch-avahi option),
# NOTE: in this case the script does not finish, it watches the changes in
# the Avahi service and updates the URL if needed
#
# issue location for the Agama SSL certificate fingerprints, it is generated as
# the last one and is used as a trigger to allow refreshing all other generated
# issues
CERT_ISSUE=/run/issue.d/50-agama-ssl-certificate.issue
if grep -q "\binst.remote=0\b" /run/agama/cmdline.d/agama.conf; then
LOCAL_ONLY=1
else
LOCAL_ONLY=0
fi
# a helper function which generates the Agama welcome message displayed at the
# console
generate_welcome() {
# get the latest version of any Agama package (except the integration tests, it lives in a
# separate git repository and has different number of commits than the rest)
AGAMA_VERSION=$(rpm -qa | grep agama | grep -v agama-integration-tests | xargs rpm -q --queryformat \
"%{VERSION}\n" | sed -e "s/\\.devel/+/" -e 's/+0$//' | sort -V | tail -n 1)
ISSUE=/run/issue.d/10-agama-welcome.issue
printf "Welcome to \\\\e{lightgreen}Agama\\\\e{reset} installer version %s! (\\\\l)\n\n" "$AGAMA_VERSION" > "$ISSUE"
}
# a helper function which displays the SSH host key fingerprints at the console
generate_ssh_fingerprints() {
FINGERPRINTS=$(find /etc/ssh -type f -name "ssh_host_*_key" -exec ssh-keygen -l -f \{\} \; | cut -d ' ' -f 2,4 | sed -e "s/^/ /")
ISSUE=/run/issue.d/30-live-ssh-fingerprints.issue
printf "SSH host key fingerprints:\n%s\n\n" "$FINGERPRINTS" > "$ISSUE"
}
# a helper function which generates the Agama SSL certificate fingerprints
# displayed at the console
generate_certificate_fingerprints() {
if [ "$LOCAL_ONLY" = "1" ]; then
echo "Remote access to the Agama installer is disabled, not generating Agama SSL fingerprints"
exit 0
fi
AGAMA_CERT=/etc/agama.d/ssl/cert.pem
# delete the previous file if it is there
rm -f "$CERT_ISSUE"
if [ -e "$AGAMA_CERT" ]; then
SHA256=$(openssl x509 -fingerprint -sha256 -noout -in "$AGAMA_CERT" | sed -e "s/^sha256 Fingerprint=//")
SHA1=$(openssl x509 -fingerprint -sha1 -noout -in "$AGAMA_CERT" | sed -e "s/^sha1 Fingerprint=//")
if [ -n "$SHA256" ] && [ -n "$SHA1" ]; then
printf "Agama installer SSL certificate fingerprints:\n SHA256: %s\n SHA1: %s\n\n" "$SHA256" "$SHA1" \
> "$CERT_ISSUE"
fi
fi
# tell agetty to use the issues from /run
touch /run/issue
# reload the issues to activate the changes
touch /run/agetty.reload
}
# message file for the Agama mDNS URL
AVAHI_MESSAGE=/run/issue.d/70-agama-connect-avahi.message
# symlink for the issue file, created only when network is available
AVAHI_ISSUE=/run/issue.d/70-agama-connect-avahi.issue
# issue file with Agama URLs
URL_ISSUES="/run/issue.d/70-agama-connect-urls.issue"
# issue displayed when there is no network connection
DISCONNECTED_ISSUE="/run/issue.d/70-agama-disconnected.issue"
# helper function, build the Agama URL messages or display a warning when
# network is not available
write_url_headers() {
# generate a header and footer around the Agama URL issues
ISSUE_HEADER=/run/issue.d/69-agama-connect.issue
ISSUE_FOOTER=/run/issue.d/71-agama-connect.issue
if [ -e "$URL_ISSUES" ]; then
# if Avahi URL is set then display it as well
if [ -e "$AVAHI_MESSAGE" ]; then
ln -sf "$AVAHI_MESSAGE" "$AVAHI_ISSUE"
fi
rm -f "$DISCONNECTED_ISSUE"
# at least one address present, display the header and footer
echo "Connect to the Agama installer using these URLs:" > "$ISSUE_HEADER"
echo > "$ISSUE_FOOTER"
else
# no network, delete the header, footer and the Avahi issue symlink
rm -f "$ISSUE_HEADER" "$ISSUE_FOOTER" "$AVAHI_ISSUE"
# display a warning message
printf "\\\\e{brown}Network is not available, the Agama installer cannot \
be used remotely.\\\\e{reset}\n\n" > "$DISCONNECTED_ISSUE"
fi
}
# a helper function which generates the mDNS URL for accessing the Agama server
# displayed at the console
generate_avahi_url() {
if [ "$LOCAL_ONLY" = "1" ]; then
echo "Remote access to the Agama installer is disabled, skipping Avahi URL generation"
exit 0
fi
# track the name, update the issue file only if the name is changed
OLDNAME=""
# watch for a systemd signal describing the status message change from the Avahi daemon
dbus-monitor --system "sender='org.freedesktop.systemd1',\
interface='org.freedesktop.DBus.Properties',\
path='/org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice',type=signal" \
2> /dev/null | while read -r line;
do
AVAHINAME=$(echo "$line" | grep "Server startup complete. Host name is" \
| sed -e "s/^.*Server startup complete. Host name is \(.*\)\. Local.*$/\\1/")
# mDNS host name found and it is different than the previous one (or the initial value)
if [ -n "$AVAHINAME" ] && [ "$AVAHINAME" != "$OLDNAME" ]; then
OLDNAME="$AVAHINAME"
echo " https://$AVAHINAME" > "$AVAHI_MESSAGE"
write_url_headers
# reload if not in the initial state
if [ -e "$CERT_ISSUE" ]; then
touch /run/agetty.reload
fi
fi
# daemon stopped, remove the message file
if echo "$line" | grep -q "avahi-daemon .* exiting"; then
OLDNAME=""
rm -f "$AVAHI_MESSAGE"
write_url_headers
touch /run/agetty.reload
fi
done
}
# function for centering text
# $1 - the text
# $2 - requested width
function center_text() {
LEN=${#1}
PADDING_LEN=$(($2 - LEN))
PADDING_LEN=$((PADDING_LEN / 2))
PADDING="$(printf '%*s' $PADDING_LEN)"
echo "$PADDING$1$PADDING"
}
# generate QR codes for the access URLs
function create_qr_codes() {
ADDRESSES=("$@")
# width of the generated QR code
QR_WIDTH=30
# check if serial console is used, get the terminal size (width)
TERM_WIDTH=$(stty -F /dev/ttyS0 size 2> /dev/null | cut -d " " -f 2)
# otherwise check the first console size
if [ -z "$TERM_WIDTH" ]; then
TERM_WIDTH=$(stty -F /dev/tty1 size 2> /dev/null | cut -d " " -f 2)
echo "Linux console width $TERM_WIDTH"
else
echo "Serial console width $TERM_WIDTH"
fi
# display QR codes only if the terminal is bigger than the 80x24(25) default
if [ -n "$TERM_WIDTH" ] && [ "$TERM_WIDTH" -gt 80 ]; then
# compute how much QR codes can fit on the screen side-by-side
QR_NUM=$(( TERM_WIDTH / QR_WIDTH ))
# split the list into the IPv4 and IPv6 addresses, display QR codes only for the IPv4 addresses
IP4ADDR=()
IP6ADDR=()
for ADDR in "${ADDRESSES[@]}"; do [[ "$ADDR" =~ : ]] && IP6ADDR+=("$ADDR") || IP4ADDR+=("$ADDR"); done
# split the IPv4 list into 2 parts, for the first part display the QR codes,
QR_ADDRESSES=("${IP4ADDR[@]:0:$QR_NUM}")
SZ="${#IP4ADDR[@]}"
# for the rest display just the text URL as the list is too long
REST_ADDRESSES=("${IP4ADDR[@]:$QR_NUM:$SZ}")
else
QR_ADDRESSES=()
REST_ADDRESSES=()
IP6ADDR=()
# put IPv6 addresses into a separate list to display [] around them
for ADDR in "${ADDRESSES[@]}"; do
[[ "$ADDR" =~ : ]] && IP6ADDR+=("$ADDR") || REST_ADDRESSES+=("$ADDR")
done
fi
if [ -n "${REST_ADDRESSES[*]}" ]; then
printf " https://%s\n" "${REST_ADDRESSES[@]}" >> "$URL_ISSUES"
fi
if [ -n "${IP6ADDR[*]}" ]; then
printf " https://[%s]\n" "${IP6ADDR[@]}" >> "$URL_ISSUES"
fi
if [ -z "${QR_ADDRESSES[*]}" ]; then
echo >> "$URL_ISSUES"
return 0
fi
# temporary file for generated QR code
QR_TEMP=$(mktemp)
# temporary file for merged QR codes (displayed side-by-side)
QR_RESULT=$(mktemp)
# copy of the merged QR codes (the merged file cannot be used as input and
# output at the same time)
QR_RESULT_COPY=$(mktemp)
# label with URLs displayed below the QR codes
LABEL=""
# generate the QR codes and merge them side-by-side
for ADDR in "${QR_ADDRESSES[@]}"; do
cp "$QR_RESULT" "$QR_RESULT_COPY"
URL="https://$ADDR"
echo "Rendering QR code for $URL"
# force (the -v option) using at least symbol version 2 (QR size 25x25),
# for short addresses (like https://1.1.1.1) it would be enough using
# version 1 (QR size 21x21), but longer addresses need version 2 and
# putting different sizes side-by-side breaks formatting, see `man
# qrencode` and https://www.qrcode.com/en/about/version.html
qrencode -t ANSIUTF8 -m 2 -v 2 -o "$QR_TEMP" "$URL"
# put the QR codes side-by-side
paste -d " " "$QR_RESULT_COPY" "$QR_TEMP" > "$QR_RESULT"
PADDED_URL=$(center_text "$URL" "$QR_WIDTH")
LABEL="$LABEL$PADDED_URL "
done
cat "$QR_RESULT" >> "$URL_ISSUES"
echo "$LABEL" >> "$URL_ISSUES"
echo >> "$URL_ISSUES"
# delete the temporary files
rm -f "$QR_TEMP" "$QR_RESULT_COPY" "$QR_RESULT"
}
# helper function, write the issue with the currently available URLs for
# accessing Agama from outside
build_addresses() {
ADDRESSES=()
readarray -t CONNECTIONS < <(busctl -j get-property org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager ActiveConnections | jq --raw-output ".data[]")
for CONNECTION in "${CONNECTIONS[@]}"; do
TYPE=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Type 2> /dev/null | jq --raw-output ".data")
# ignore loopbacks, we need external addresses
if [ "$TYPE" != "loopback" ]; then
IP4CONFIG=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Ip4Config 2> /dev/null | jq --raw-output ".data")
mapfile -t IP4ADDRESSES < <(busctl -j get-property org.freedesktop.NetworkManager "$IP4CONFIG" org.freedesktop.NetworkManager.IP4Config AddressData 2> /dev/null | jq --raw-output ".data[].address.data")
ADDRESSES+=("${IP4ADDRESSES[@]}")
IP6CONFIG=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Ip6Config 2> /dev/null | jq --raw-output ".data")
# ignore IPv6 link local addresses starting with "fe80:", they are not supported by browsers
mapfile -t IP6ADDRESSES < <(busctl -j get-property org.freedesktop.NetworkManager "$IP6CONFIG" org.freedesktop.NetworkManager.IP6Config AddressData 2> /dev/null | jq --raw-output ".data[].address.data" | grep -i -v ^fe80:)
ADDRESSES+=("${IP6ADDRESSES[@]}")
fi
done
# remove duplicates
readarray -t ADDRESSES < <(printf "%s\n" "${ADDRESSES[@]}" | sort -u)
echo "Found external addresses: ${ADDRESSES[*]}"
# delete the old file
rm -f "$URL_ISSUES"
if [ -n "${ADDRESSES[*]}" ]; then
create_qr_codes "${ADDRESSES[@]}"
fi
write_url_headers
# reload if not in the initial state
if [ -e "$CERT_ISSUE" ]; then
touch /run/agetty.reload
fi
}
# a helper function which generates the URLs for accessing the Agama server
# displayed at the console
generate_network_url() {
if [ "$LOCAL_ONLY" = "1" ]; then
echo "Remote access to the Agama installer is disabled, skipping network URL generation"
# display a warning message
printf "\\\\e{brown}Remote access to the Agama installer is disabled, it can be used only locally.\\\\e{reset}\n\n" > "$DISCONNECTED_ISSUE"
exit 0
fi
# build a message with the current URLs
build_addresses
# watch for IP address changes in the NetworkManager service
dbus-monitor --system "sender='org.freedesktop.NetworkManager',\
interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',\
type=signal" 2> /dev/null | while read -r line; do
# some IP4 configuration has been changed, rebuild the URLs
if echo "$line" | grep -q -E 'string "org.freedesktop.NetworkManager.IP(4|6)Config"'; then
echo "Network configuration changed"
build_addresses
fi
done
}
# wait until the SSL fingreprint issue is created with a time limit passed as
# the second argument (in seconds)
wait_for_ssl_issue() {
for _ in $(seq 1 "$1"); do
[ -f "$CERT_ISSUE" ] && exit 0
sleep 1
done
}
# make sure the parent directory for the issues exists
mkdir -p /run/issue.d
# create the issue file for specified item
if [ "$1" = "--welcome" ]; then
generate_welcome
elif [ "$1" = "--ssh" ]; then
generate_ssh_fingerprints
elif [ "$1" = "--ssl" ]; then
generate_certificate_fingerprints
elif [ "$1" = "--wait-for-ssl" ]; then
wait_for_ssl_issue "$2"
elif [ "$1" = "--watch-network" ]; then
generate_network_url "$2" "$3"
elif [ "$1" = "--watch-avahi" ]; then
generate_avahi_url
else
echo "Missing or incorrect argument"
exit 1
fi