diff --git a/os/debian/Dockerfile.debian b/os/debian/Dockerfile.debian index a76bb21a..3c1eb3b2 100644 --- a/os/debian/Dockerfile.debian +++ b/os/debian/Dockerfile.debian @@ -236,8 +236,7 @@ RUN helm3 plugin install https://github.com/databus23/helm-diff.git --version v$ # AWS_DATA_PATH is a PATH-like variable for configuring the AWS botocore library to # load additional modules. Do not set it. ARG GEODESIC_AWS_HOME=${HOME}/.aws -ENV AWS_CONFIG_FILE=${GEODESIC_AWS_HOME}/config -ENV AWS_SHARED_CREDENTIALS_FILE=${GEODESIC_AWS_HOME}/credentials + # Region abbreviation types are "fixed" (always 3 chars), "short" (4-5 chars), or "long" (the full AWS string) # See https://github.com/cloudposse/terraform-aws-utils#introduction ENV AWS_REGION_ABBREVIATION_TYPE=short @@ -320,6 +319,14 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ sudo dpkg -i /tmp/session-manager-plugin.deb && \ rm -f /tmp/session-manager-plugin.deb +# This is a workaround for https://github.com/moby/buildkit/issues/5775 +# CHAMBER_KMS_KEY_ALIAS is used by the `chamber` CLI, but it is incorrectly +# flagged as a secret by the SecretsUsedInArgOrEnv check. This is a false positive. +# So, as a workaround, we allow you to set `CHAMBER_KMS_ALIAS` instead, +# and at runtime we copy the value to `CHAMBER_KMS_KEY_ALIAS` for you. +ENV CHAMBER_KMS_ALIAS=aws/ssm + + # Install documentation COPY docs/ /usr/share/docs/ diff --git a/packages.txt b/packages.txt index fc916db9..134b54d7 100644 --- a/packages.txt +++ b/packages.txt @@ -40,6 +40,7 @@ kubectl@cloudposse kubectx@cloudposse kubens@cloudposse less +lsb-release make man-db openssh-client diff --git a/rootfs/etc/profile.d/_10-colors.sh b/rootfs/etc/profile.d/_10-colors.sh index 158a0030..fe247c0d 100755 --- a/rootfs/etc/profile.d/_10-colors.sh +++ b/rootfs/etc/profile.d/_10-colors.sh @@ -10,6 +10,28 @@ # The main change is that it uses the terminal's default colors for foreground and background, # whereas the previous version "reset" the color by setting it to black, which fails in dark mode. +# These utilities use basic ANSI color codes (0-7) to colorize text in the terminal. +# Besides being the most widely supported method, it also has the advantage +# that terminals that support colored text and backgrounds as themes often +# take responsibility for the ANSI colors being legible regardless of the theme. +# For example, on a completely red background, the ANSI red color will be changed +# as part of the theme to a lighter or darker shade of red to ensure it is legible. +# This would not be the case if we tried to set the color directly using RGB values, +# or even using extended ANSI colors. + +# To test a terminal manually, the following script displays all 256 ANSI colors. +# Our color functions only use 1 through 6. +# We indirectly support 0 and 7 (black and white), reversing them in dark mode, +# but we do not provide direct functions for them. +# We support a "bold" modifier, which some terminals equate to colors 8 through 15. +# +# for i in {0..255}; do +# printf "\e[38;5;${i}mcolor%-5i\e[0m" $i +# if [ $((($i + 1) % 8)) == 0 ]; then +# echo +# fi +# done + function update-terminal-theme() { local new_mode="$1" local quiet=false diff --git a/rootfs/etc/profile.d/aws.sh b/rootfs/etc/profile.d/aws.sh index c535cd73..c8431fd1 100755 --- a/rootfs/etc/profile.d/aws.sh +++ b/rootfs/etc/profile.d/aws.sh @@ -1,4 +1,7 @@ #!/bin/bash +# shellcheck disable=SC2155 +# Above directive suppresses ShellCheck SC2155: Declare and assign separately to avoid masking return values. +# In this script, we do not care about return values, as problems are detected by the resulting empty value. export AWS_REGION_ABBREVIATION_TYPE=${AWS_REGION_ABBREVIATION_TYPE:-fixed} export AWS_DEFAULT_SHORT_REGION=${AWS_DEFAULT_SHORT_REGION:-$(aws-region --${AWS_REGION_ABBREVIATION_TYPE} ${AWS_DEFAULT_REGION:-us-west-2})} @@ -62,7 +65,7 @@ fi function aws_choose_role() { _preview="${FZF_PREVIEW:-crudini --format=ini --get "$AWS_CONFIG_FILE" 'profile {}'}" - cat "${AWS_SHARED_CREDENTIALS_FILE:-~/.aws/credentials}" "${AWS_CONFIG_FILE:-~/.aws/config}" 2>/dev/null | + cat "${AWS_SHARED_CREDENTIALS_FILE:-${GEODESIC_AWS_HOME}/credentials}" "${AWS_CONFIG_FILE:-${GEODESIC_AWS_HOME}/config}" 2>/dev/null | crudini --get - | sed 's/^ *profile *//' | fzf \ --height 30% \ @@ -107,7 +110,7 @@ function aws_sdk_assume_role() { # Asks AWS what the currently active identity is and # sets environment variables accordingly function export_current_aws_role() { - local role_name + local role_name role_names # Could be a primary or assumed role. If we have assumed a role, cut off the session name. local current_role=$(aws sts get-caller-identity --output text --query 'Arn' 2>/dev/null | cut -d/ -f1-2) if [[ -z $current_role ]]; then @@ -115,66 +118,132 @@ function export_current_aws_role() { return 0 fi - # Quick check, are we who we say we are? + # If AWS_VAULT is not enabled, clear any setting from it. + [[ "${AWS_VAULT_ENABLED:-false}" == "true" ]] || unset AWS_VAULT + + # Quick check, are we who we say we are? Does the current role match the profile? local profile_arn local profile_target=${AWS_PROFILE:-${AWS_VAULT:-default}} - if [[ -n $profile_target ]]; then - profile_arn=$(aws --profile "${profile_target}" sts get-caller-identity --output text --query 'Arn' 2>/dev/null | cut -d/ -f1-2) - if [[ $profile_arn == $current_role ]]; then - # Extract profile name from config file: - # 1. For default profile, look for a better name - # 2. Skip identity profiles (ending with -identity), as they are too generic - # 3. Use the first non-default, non-identity profile found - if [[ $profile_target == "default" ]] || [[ $profile_target =~ -identity$ ]]; then - # Make some effort to find a better name for the role, but only check the config file, not credentials. - local config_file="${AWS_CONFIG_FILE:-\~/.aws/config}" - if [[ -r $config_file ]]; then - # Assumed roles in AWS config file use the role ARN, not the assumed role ARN, so adjust accordingly. - local role_arn=$(printf "%s" "$current_role" | sed 's/:sts:/:iam:/g' | sed 's,:assumed-role/,:role/,') - role_name=($(crudini --get --format=lines "$config_file" | grep "$role_arn" | cut -d' ' -f 3)) - for rn in "${role_name[@]}"; do - if [[ $rn == "default" ]] || [[ $rn =~ -identity$ ]]; then + # Remove the session name from the profile target role, if present + profile_arn=$(aws --profile "${profile_target}" sts get-caller-identity --output text --query 'Arn' 2>/dev/null | cut -d/ -f1-2) + # The main way there would be a mismatch is if AWS_VAULT is set or there are API keys in the environment + if [[ "$profile_arn" == "$current_role" ]]; then + # If we are here, then the current role matches the assigned profile. That is a good thing. + # However, the profile name may not be the best name for the role. If it is too generic, try to find a better name. + # Extract profile name from config file: + # 1. For default profile, look for a better name + # 2. Skip identity profiles (ending with -identity), as they are too generic + # 3. Use the first non-default, non-identity profile found + if [[ $profile_target == "default" ]] || [[ $profile_target =~ -identity$ ]]; then + local backup_name="$profile_target" + # Make some effort to find a better name for the role, but only check the config file, not credentials. + local config_file="${AWS_CONFIG_FILE:-${GEODESIC_AWS_HOME}/config}" + if [[ -r $config_file ]]; then + # Is this a normal IAM role or an Identity Center permissions set role? + if [[ $current_role =~ AWSReservedSSO_[^_]+_[0-9a-f]+$ ]]; then + # This is an Identity Center permissions set role + # current_role is "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_IdentityAdminRoleAccess_5c90026c17fbd1c2" + + # Extract account ID using cut + local account_id=$(echo "$current_role" | cut -d':' -f5) + + # Extract the full role part + local role_part=$(echo "$current_role" | cut -d':' -f6) # This gets everything after the 5th colon + + # Extract the role name by isolating it from boilerplate + local sso_role_name=$(echo "$role_part" | cut -d'_' -f2) # This selects the second field delimited by '_' + + # Find all profiles that have matching role names + local profile_names=($(crudini --get --format=lines "$config_file" | grep "$sso_role_name" | cut -d' ' -f 3)) + local profile_name + for profile_name in "${profile_names[@]}"; do + # Skip the generic profiles + if [[ "$profile_name" == "default" ]] || [[ "$profile_name" =~ -identity$ ]]; then continue - else - export ASSUME_ROLE=$rn + fi + if [[ "$account_id" == "$(crudini --get "$config_file" "profile $profile_name" sso_account_id)" ]]; then + export ASSUME_ROLE="$profile_name" return fi done + export ASSUME_ROLE="$backup_name" + return fi - else - export ASSUME_ROLE="$profile_target" - return + + # Normal IAM role + # Assumed roles in AWS config file use the role ARN, not the assumed role ARN, so adjust accordingly. + local role_arn=$(printf "%s" "$current_role" | sed 's/:sts:/:iam:/g' | sed 's,:assumed-role/,:role/,') + role_names=($(crudini --get --format=lines "$config_file" | grep "$role_arn" | cut -d' ' -f 3)) + for rn in "${role_names[@]}"; do + if [[ $rn == "default" ]] || [[ $rn =~ -identity$ ]]; then + continue + else + export ASSUME_ROLE=$rn + return + fi + done fi fi - echo "* $(red Profile is set to $profile_target but current role does not match:)" - echo "* $(red $current_role)" + # could not find a better match, so just use the generic profile name + export ASSUME_ROLE="$profile_target" + return + fi + + # If we are here, then the current role is not what we would expect from the AWS_PROFILE setting. + # If AWS_PROFILE is unset, then we forgive the current role not being the default role. + # Otherwise, we warn about a mismatch. + if [[ -n $AWS_PROFILE ]]; then + red "* AWS Credentials Mismatch! AWS_PROFILE is set to $AWS_PROFILE" + red "* That profile selects role $profile_arn" + red "* But STS reports current role is $current_role" + export ASSUME_ROLE=$(red-n '!mixed!') + return + elif [[ -n $AWS_VAULT ]]; then + red "* AWS Credentials Mismatch! AWS_VAULT claims to have set role to profile $AWS_VAULT" + red "* That profile selects role $profile_arn" + red "* But STS reports current role is $current_role" + red "* " export ASSUME_ROLE=$(red-n '!mixed!') return fi + # If we are here, then we are not using AWS_VAULT or AWS_PROFILE, and the current role does not match the default profile. + # This is likely because we are using API keys directly in the environment or credentials file. + # Try to figure out a better name for the role. + # saml2aws will store the assumed role from sign-in as x_principal_arn in credentials file # Default values from https://awscli.amazonaws.com/v2/documentation/api/latest/topic/config-vars.html - local creds_file="${AWS_SHARED_CREDENTIALS_FILE:-\~/.aws/credentials}" + local creds_file="${AWS_SHARED_CREDENTIALS_FILE:-${GEODESIC_AWS_HOME}/credentials}" if [[ -r $creds_file ]]; then role_name=$(crudini --get --format=lines "${creds_file}" | grep "$current_role" | head -1 | cut -d' ' -f 2) fi # Assumed roles are normally found in AWS config file, but using the role ARN, # not the assumed role ARN. google2aws also puts login role in this file. - local config_file="${AWS_CONFIG_FILE:-\~/.aws/config}" + local config_file="${AWS_CONFIG_FILE:-${GEODESIC_AWS_HOME}/config}" if [[ -z $role_name ]] && [[ -r $config_file ]]; then local role_arn=$(printf "%s" "$current_role" | sed 's/:sts:/:iam:/g' | sed 's,:assumed-role/,:role/,') role_name=$(crudini --get --format=lines "$config_file" | grep "$role_arn" | head -1 | cut -d' ' -f 3) fi + # If we still don't have a profile name, make one up. if [[ -z $role_name ]]; then if [[ "$role_arn" =~ "role/OrganizationAccountAccessRole" ]]; then role_name="$(printf "%s" "$role_arn" | cut -d: -f 5):OrgAccess" - echo "* $(red "Could not find profile name for ${role_arn} ; calling it \"${role_name}\"")" >&2 + elif [[ $current_role =~ AWSReservedSSO_[^_]+_[0-9a-f]+$ ]]; then + # This is an Identity Center permissions set role + # current_role is "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_IdentityAdminRoleAccess_5c90026c17fbd1c2" + # Extract account ID using cut + local account_id=$(echo "$current_role" | cut -d':' -f5) + # Extract the full role part + local role_part=$(echo "$current_role" | cut -d':' -f6) # This gets everything after the 5th colon + # Extract the role name by isolating it from boilerplate + local sso_role_name=$(echo "$role_part" | cut -d'_' -f2) # This selects the second field delimited by '_' + role_name="${account_id}:${sso_role_name}" else role_name="$(printf "%s" "$role_arn" | cut -d/ -f 2)" - echo "* $(green "Could not find profile name for ${role_arn} ; calling it \"${role_name}\"")" >&2 fi + echo "* $(green "Could not find profile name for ${role_arn} ; calling it \"${role_name}\"")" >&2 fi export ASSUME_ROLE="$role_name" } @@ -186,7 +255,7 @@ function refresh_current_aws_role_if_needed() { local is_exported="^declare -[^ x]*x[^ x]* " local aws_profile=$(declare -p AWS_PROFILE 2>/dev/null) [[ $aws_profile =~ $is_exported ]] || aws_profile="" - local credentials_mtime=$(stat -c "%Y" ${AWS_SHARED_CREDENTIALS_FILE:-"~/.aws/credentials"} 2>/dev/null) + local credentials_mtime=$(stat -c "%Y" "${AWS_SHARED_CREDENTIALS_FILE:-${GEODESIC_AWS_HOME}/credentials}" 2>/dev/null) local role_fingerprint="${aws_profile}/${credentials_mtime}/${AWS_ACCESS_KEY_ID}" if [[ $role_fingerprint != $GEODESIC_AWS_ROLE_CACHE ]]; then export_current_aws_role diff --git a/rootfs/etc/profile.d/chamber.sh b/rootfs/etc/profile.d/chamber.sh new file mode 100644 index 00000000..3cbc5a74 --- /dev/null +++ b/rootfs/etc/profile.d/chamber.sh @@ -0,0 +1,22 @@ +# This is a workaround for https://github.com/moby/buildkit/issues/5775 +# +# The `chamber` command (https://github.com/segmentio/chamber) is a CLI for managing secrets. +# It is installed in Geodesic and referenced in various documentation. +# +# Chamber works with AWS SSM Parameter store to save encrypted parameters. +# By default, in uses a KMS key with the alias `parameter_store_key` +# to encrypt and decrypt the parameters. Geodesic supports using +# the AWS default key, with alias `ssm`, or a custom key. +# +# However, due to the issue with buildkit mentioned above, +# setting the required environment variable in the Dockerfile +# leads to a warning about it being a secret stored in the image. +# +# So, as a workaround, we allow you to set `CHAMBER_KMS_ALIAS` instead, +# and we will set the `CHAMBER_KMS_KEY_ALIAS` environment variable for you here. +# + +if [[ -z "$CHAMBER_KMS_KEY_ALIAS" ]] && [[ -n "$CHAMBER_KMS_ALIAS" ]]; then + export CHAMBER_KMS_KEY_ALIAS="$CHAMBER_KMS_ALIAS" + unset CHAMBER_KMS_ALIAS +fi