From 77a1d55f661cda89b8916ed8612d1cc78910705a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jake=20Buchholz=20G=C3=B6kt=C3=BCrk?= Date: Sat, 9 May 2026 21:24:10 +0000 Subject: [PATCH] per-login GCP SSH keys --- CHANGELOG.md | 10 +++++- lib/tiny-cloud/cloud/gcp/imds | 4 +-- lib/tiny-cloud/cloud/gcp/init | 58 +++++++++++++++++++++++++++++++++++ tests/init.test | 36 ++++++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 lib/tiny-cloud/cloud/gcp/init diff --git a/CHANGELOG.md b/CHANGELOG.md index d456708..b828a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # CHANGELOG -## 2025:12-07 - Tiny Cloud v3.2.3 +## XXXX-XX-XX - Tiny Cloud v3.X.X + +#### GOOGLE CLOUD BEHAVIOR CHANGE +* ***NOTE***: Google Cloud SSH keys are installed for per specified logins, + instead of dumping them all into `$CLOUD_USER`'s `.ssh/authorized_keys`. + This matches `cloud-init`'s behavior and Google's intentions. Resolves + [#73](https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud/-/issues/73). + +## 2025-12-07 - Tiny Cloud v3.2.3 * Correctly identify empty `user-data` content instead of flagging it as "unknown". diff --git a/lib/tiny-cloud/cloud/gcp/imds b/lib/tiny-cloud/cloud/gcp/imds index 56853fb..66a06fc 100644 --- a/lib/tiny-cloud/cloud/gcp/imds +++ b/lib/tiny-cloud/cloud/gcp/imds @@ -21,8 +21,8 @@ _imds_header() { _imds_ssh_keys() { local ssh_keys + # NOTE: keys are prefixed with ":" for ssh_keys in $IMDS_SSH_KEYS; do - # ignore errors and strip leading ':' - imds -e "$ssh_keys" | cut -d: -f2- + imds -e "$ssh_keys" done | sort -u } diff --git a/lib/tiny-cloud/cloud/gcp/init b/lib/tiny-cloud/cloud/gcp/init new file mode 100644 index 0000000..43977a1 --- /dev/null +++ b/lib/tiny-cloud/cloud/gcp/init @@ -0,0 +1,58 @@ +# GCP - Init Functions +# vim:set filetype=sh: +# shellcheck shell=sh + +# NOTE: overrides lib/tiny-cloud function +# GCP ssh keys have a leading ":" we should check/honor +init__set_ssh_keys() { + mkdir -p "$ROOT/run/tiny-cloud" + local tmp_dir=$(mktemp -d "$ROOT/run/tiny-cloud/sshkeys-XXXXXX") + chmod 700 "$tmp_dir" + local userkey + local user + local key + local pwent + local group + local tmp_file + local found= + imds @ssh-keys | while IFS= read -r userkey; do + user=$(echo "$userkey" | cut -d: -f1) + key=$(echo "$userkey" | cut -d: -f2-) + if [ -z "$user" ] || ! pwent="$(getent passwd "$user")"; then + log -i -t "$phase" warning "$ACTION: skipping SSH key for $user" + continue + fi + group=$(echo "$pwent" | cut -d: -f4) + tmp_file="$tmp_dir/$user" + touch "$tmp_file" + chmod 600 "$tmp_file" + $MOCK chown -R "$user:$group" "$tmp_file" + echo "$key" >> "$tmp_file" + done + local ssh_dir + for tmp_file in "$tmp_dir"/*; do + [ -f "$tmp_file" ] || continue + user=$(basename "$tmp_file") + pwent="$(getent passwd "$user")" + group=$(echo "$pwent" | cut -d: -f4) + ssh_dir="$ROOT$(echo "$pwent" | cut -d: -f6)/.ssh" + if [ ! -d "$ssh_dir" ]; then + mkdir -p "$ssh_dir" + $MOCK chown -R "$user:$group" "$ssh_dir" + chmod 700 "$ssh_dir" + fi + cp -a "$tmp_file" "$ssh_dir/authorized_keys" + log -i -t "$phase" info "$ACTION: installed ssh keys for $user" + if [ "$user" = "$CLOUD_USER" ]; then + found=2 + elif [ "$found" != 2 ]; then + found=1 + fi + done + rm -rf "$tmp_dir" + if [ -z "$found" ]; then + log -i -t "$phase" warning "$ACTION: no SSH keys installed" + elif [ "$found" != 2 ]; then + log -i -t "$phase" warning "$ACTION: no SSH keys found for $CLOUD_USER" + fi +} diff --git a/tests/init.test b/tests/init.test index 258f2fa..158468f 100755 --- a/tests/init.test +++ b/tests/init.test @@ -21,6 +21,7 @@ init_tests \ save_userdata_compressed \ set_hostname \ set_ssh_keys \ + set_ssh_keys_gcp \ userdata_type \ run_userdata \ autodetect_aws_cmdline \ @@ -189,6 +190,41 @@ set_ssh_keys_body() { cat home/alpine/.ssh/authorized_keys } +set_ssh_keys_gcp_body() { + fake_bin getent <<-EOF + #!/bin/sh + cat <<-EOD | grep "^\$2:" + alpine:x:1000:1000:Linux User,,,:/home/alpine:/bin/sh + bar:x:1001:1001:Bar User,,,:/home/bar:/bin/sh + EOD + EOF + fake_metadata_gcp <<-EOF + project: + attributes: + ssh-keys: |- + alpine:ssh-ed25519 foobar1 alpine + foo:ssh-rsa foobar2 jake + instance: + attributes: + ssh-keys: |- + bar:ecdsa-sha2-nistp521 foobar3 bar@omfg.local + :ssh-rsa foobar4 + alpine:ssh-rsa foobar5 google-ssh {"userName":"alpine","expireOn":"2061-12-04T20:12:00+0000"} + EOF + CLOUD="gcp" atf_check \ + -e ignore \ + -o match:"chown -R bar:1001 .*\\.ssh" \ + -o not-match:"no SSH keys found for alpine" \ + sh -c ". \"$lib\"; init__set_ssh_keys" + atf_check -o match:"^ssh-ed25519 foobar1 alpine" \ + -o match:"^ssh-rsa foobar5 google-ssh" \ + -o not-match:"foobar4" \ + cat home/alpine/.ssh/authorized_keys + atf_check -o match:"^ecdsa-sha2-nistp521 foobar3 bar@omfg.local$" \ + cat home/bar/.ssh/authorized_keys + atf_check -s exit:1 test -e home/foo/.ssh/authorized_keys +} + userdata_type_body() { mkdir -p var/lib/cloud for c in $PROVIDERS; do