## 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.
352 lines
13 KiB
Bash
Executable file
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
|