diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c9338..d3dc6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.6.0 + +* Use user-data file rather than IMDS directly to set public keys. ([#19]) + +[#19]: https://github.com/bottlerocket-os/bottlerocket-admin-container/pull/19 + # 0.5.0 * Use /proc to find the bash binary in sheltie. ([#8]) diff --git a/Dockerfile b/Dockerfile index 37cfdde..99becc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ RUN test -n "$IMAGE_VERSION" LABEL "org.opencontainers.image.version"="$IMAGE_VERSION" RUN yum update -y \ - && yum install -y openssh-server sudo util-linux procps-ng \ + && yum install -y openssh-server sudo util-linux procps-ng jq \ && yum clean all COPY --from=builder /opt/bash /opt/bin/ diff --git a/README.md b/README.md index d546e80..b217534 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,33 @@ For more information about how the admin container fits into the Bottlerocket op You'll need Docker 17.06.2 or later, for multi-stage build support. Then run `make`! + +## Authenticating with the Admin Container + +Starting from v0.6.0, users have the option to pass in their own ssh keys rather than the admin container relying on the AWS instance metadata service (IMDS). + +Users can add their own keys by populating the admin container's user-data with a base64-encoded JSON block. +If user-data is populated then Bottlerocket will not fetch from IMDS at all, but if user-data is not set then Bottlerocket will continue to use the keys from IMDS. + +To use custom public keys for `.ssh/authorized_keys` and/or custom CA keys for `/etc/ssh/trusted_user_ca_keys.pub` you will want to generate a JSON-structure like this: + +``` +{ + "ssh":{ + "authorized_keys":[ + "ssh-rsa EXAMPLEAUTHORIZEDPUBLICKEYHERE my-key-pair" + ], + "trusted_user_ca_keys":[ + "ssh-rsa EXAMPLETRUSTEDCAPUBLICKEYHERE authority@ssh-ca.example.com" + ] + } +} +``` + +Once you've created your JSON, you'll need to base64-encode it and set it as the value of the admin host container's user-data setting in your [instance user data toml](https://github.com/bottlerocket-os/bottlerocket#using-user-data). + +``` +[settings.host-containers.admin] +# ex: echo '{"ssh":{"authorized_keys":[]}}' | base64 +user-data = "eyJzc2giOnsiYXV0aG9yaXplZF9rZXlzIjpbXX19" +``` diff --git a/VERSION b/VERSION index b0c2058..60f6343 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.5.2 +v0.6.0 diff --git a/sshd_config b/sshd_config index aff636c..bdf5175 100644 --- a/sshd_config +++ b/sshd_config @@ -14,3 +14,6 @@ AcceptEnv LC_IDENTIFICATION LC_ALL LANGUAGE # SFTP is enabled by default in Amazon Linux 2; keeping that behavior here. Subsystem sftp /usr/libexec/openssh/sftp-server + +# Configured by user data +TrustedUserCAKeys /etc/ssh/trusted_user_ca_keys.pub diff --git a/start_admin_sshd.sh b/start_admin_sshd.sh index a49a993..0b9d287 100644 --- a/start_admin_sshd.sh +++ b/start_admin_sshd.sh @@ -4,64 +4,90 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT set -e -mkdir -p /home/ec2-user/.ssh/ -chmod 700 /home/ec2-user/.ssh/ -ssh_host_key_dir="/.bottlerocket/host-containers/admin/etc/ssh" -ssh_config_dir="/home/ec2-user/.ssh" +log() { + echo "$*" >&2 +} -# Populate authorized_keys with all the public keys found in instance meta-data -# The URLs for keys include an index and the keypair name, e.g. -# http://169.254.169.254/latest/meta-data/public-keys/0=mykeypair/openssh-key -ssh_authorized_keys="${ssh_config_dir}/authorized_keys" -touch ${ssh_authorized_keys} -chmod 600 ${ssh_authorized_keys} -public_key_base_url="http://169.254.169.254/latest/meta-data/public-keys/" -imds_session_token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60") -imds_request_add_header="X-aws-ec2-metadata-token: ${imds_session_token}" -public_key_indexes=($(curl -H "${imds_request_add_header}" -sf "${public_key_base_url}" \ - | cut -d= -f1 \ - | xargs)) +declare -r local_user="ec2-user" +declare -r ssh_host_key_dir="/.bottlerocket/host-containers/admin/etc/ssh" +declare -r user_data="/.bottlerocket/host-containers/admin/user-data" +declare -r user_ssh_dir="/home/${local_user}/.ssh" +available_auth_methods=0 -for public_key_index in "${public_key_indexes[@]}"; do - public_key_data="$(curl -H "${imds_request_add_header}" -sf "${public_key_base_url}/${public_key_index}/openssh-key")" - if [[ ! "${public_key_data}" =~ ^"ssh" ]]; then - echo "Key ${public_key_data} with index ${public_key_index} looks invalid" >&2 - continue - fi - echo "${public_key_data}" >> "${ssh_authorized_keys}" - if ! grep -q "${public_key_data}" "${ssh_authorized_keys}"; then - echo "Failed to write key with index ${public_key_index} to authorized_keys" >&2 - continue - fi -done +mkdir -p "${user_ssh_dir}" +chmod 700 "${user_ssh_dir}" + +get_user_data_keys() { + # Extract the keys from user-data json + local raw_keys + local key_type="${1:?}" + if ! raw_keys=$(jq --arg key_type "${key_type}" -e -r '.["ssh"][$key_type][]' "${user_data}" 2>/dev/null); then + log "Failed to parse ${key_type} from ${user_data}" + return 1 + fi + + # Map the keys to avoid improper splitting + local mapped_keys + mapfile -t mapped_keys <<< "${raw_keys}" + + # Verify the keys are valid + local key + local -a valid_keys + for key in "${mapped_keys[@]}"; do + if ! echo "${key}" | ssh-keygen -lf - &>/dev/null; then + log "Failed to validate ${key}" + continue + fi + valid_keys+=( "${key}" ) + done + + ( IFS=$'\n'; echo "${valid_keys[*]}" ) +} -# If we didn't write any keys at all, there's not much point in continuing -if [ ! -s "${ssh_authorized_keys}" ]; then - echo "Failed to write any valid public keys to authorized_keys" >&2 - exit 1 +# Populate authorized_keys with all the authorized keys found in user-data +if authorized_keys=$(get_user_data_keys "authorized_keys"); then + ssh_authorized_keys="${user_ssh_dir}/authorized_keys" + touch "${ssh_authorized_keys}" + chmod 600 "${ssh_authorized_keys}" + echo "${authorized_keys}" > "${ssh_authorized_keys}" + ((++available_auth_methods)) fi -chown ec2-user -R "${ssh_config_dir}" +# Populate trusted_user_ca_keys with all the authorized keys found in user-data +if trusted_user_ca_keys=$(get_user_data_keys "trusted_user_ca_keys"); then + ssh_trusted_user_ca_keys="/etc/ssh/trusted_user_ca_keys.pub" + touch "${ssh_trusted_user_ca_keys}" + chmod 600 "${ssh_trusted_user_ca_keys}" + echo "${trusted_user_ca_keys}" > "${ssh_trusted_user_ca_keys}" + ((++available_auth_methods)) +fi + +chown "${local_user}" -R "${user_ssh_dir}" + +# If there were no successful auth methods, then users cannot authenticate +if [[ "${available_auth_methods}" -eq 0 ]]; then + log "Failed to configure ssh authentication" +fi # Generate the server keys mkdir -p "${ssh_host_key_dir}" for key in rsa ecdsa ed25519; do - # If both of the keys exist, don't overwrite them - if [ -s "${ssh_host_key_dir}/ssh_host_${key}_key" ] && [ -s "${ssh_host_key_dir}/ssh_host_${key}_key.pub" ]; then - echo "${key} key already exists, will use existing key." >&2 - continue - fi + # If both of the keys exist, don't overwrite them + if [ -s "${ssh_host_key_dir}/ssh_host_${key}_key" ] && + [ -s "${ssh_host_key_dir}/ssh_host_${key}_key.pub" ]; then + log "${key} key already exists, will use existing key." + continue + fi - rm -rf \ - "${ssh_host_key_dir}/ssh_host_${key}_key" \ - "${ssh_host_key_dir}/ssh_host_${key}_key.pub" - if ssh-keygen -t "${key}" -f "${ssh_host_key_dir}/ssh_host_${key}_key" -q -N ""; then - chmod 600 "${ssh_host_key_dir}/ssh_host_${key}_key" - chmod 644 "${ssh_host_key_dir}/ssh_host_${key}_key.pub" - else - echo "Failure to generate host ${key} ssh keys" >&2 - exit 1 - fi + rm -rf \ + "${ssh_host_key_dir}/ssh_host_${key}_key" \ + "${ssh_host_key_dir}/ssh_host_${key}_key.pub" + if ssh-keygen -t "${key}" -f "${ssh_host_key_dir}/ssh_host_${key}_key" -q -N ""; then + chmod 600 "${ssh_host_key_dir}/ssh_host_${key}_key" + chmod 644 "${ssh_host_key_dir}/ssh_host_${key}_key.pub" + else + log "Failure to generate host ${key} ssh keys" + fi done # Start a single sshd process in the foreground