From c4b62ddeeee455d1429d48dbc42820a7df48c031 Mon Sep 17 00:00:00 2001 From: jmug Date: Mon, 14 Jul 2025 18:16:56 -0700 Subject: [PATCH] Add aws app and config for mfa. Signed-off-by: jmug --- home-modules/hyprland/hypridle.nix | 3 +- home-modules/sops.nix | 2 + hosts/asahi/home.nix | 14 +++ nixos-modules/shell-apps/aws-cli-mfa.nix | 146 +++++++++++++++++++++++ secrets.yaml | 7 +- 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 nixos-modules/shell-apps/aws-cli-mfa.nix diff --git a/home-modules/hyprland/hypridle.nix b/home-modules/hyprland/hypridle.nix index 0aa4809..683eca8 100644 --- a/home-modules/hyprland/hypridle.nix +++ b/home-modules/hyprland/hypridle.nix @@ -4,8 +4,9 @@ settings = { general = { lock_cmd = "pidof hyprlock || hyprlock"; # Avoid starting hyprlock multiple times. - before_sleep_cmd = "loginctl lock-session"; # lock before suspend. + before_sleep_cmd = "pidof hyprlock || hyprlock --no-fade-in --immediate"; # lock before suspend. after_sleep_cmd = "hyprctl dispatch dpms on"; # to avoid having to press a key twice to turn on the display. + inhibit_sleep = 3; # Wait for hyprlock. }; listener = [ { diff --git a/home-modules/sops.nix b/home-modules/sops.nix index b27c95e..e5dbc0d 100644 --- a/home-modules/sops.nix +++ b/home-modules/sops.nix @@ -20,6 +20,8 @@ "private_keys/ace" = { path = "/home/jmug/.ssh/id_ace"; }; + "aws/mfa_serial" = {}; + "aws/role_arn" = {}; }; }; } diff --git a/hosts/asahi/home.nix b/hosts/asahi/home.nix index 92f3ad0..901c5a7 100644 --- a/hosts/asahi/home.nix +++ b/hosts/asahi/home.nix @@ -62,6 +62,9 @@ in # Dev tools flyctl pkgs-unstable.claude-code + # AWS tools + awscli2 + (callPackage ../../nixos-modules/shell-apps/aws-cli-mfa.nix {}) ]; pointerCursor = { @@ -155,8 +158,19 @@ in fly = "flyctl"; # TODO: Interpolate the name of the host here. nrsw = "sudo nixos-rebuild switch --flake /home/jmug/nixos#asahi"; # parametrize this as home dir. + awsmfa = "eval $(aws-cli-mfa)"; + uawsmfa = "eval $(aws-cli-mfa --unset)"; }; # Let Home Manager install and manage itself. programs.home-manager.enable = true; + + home.activation.aws-cli-mfa-config = lib.hm.dag.entryAfter ["writeBoundary"] '' + mkdir -p ~/.config/aws-cli-mfa + cat > ~/.config/aws-cli-mfa/config.yaml << EOF +mfa_serial: $(cat ${config.sops.secrets."aws/mfa_serial".path}) +role_arn: $(cat ${config.sops.secrets."aws/role_arn".path}) +session_duration: 43200 +EOF + ''; } diff --git a/nixos-modules/shell-apps/aws-cli-mfa.nix b/nixos-modules/shell-apps/aws-cli-mfa.nix new file mode 100644 index 0000000..fa54d44 --- /dev/null +++ b/nixos-modules/shell-apps/aws-cli-mfa.nix @@ -0,0 +1,146 @@ +{ pkgs }: +pkgs.writeShellApplication { + name = "aws-cli-mfa"; + runtimeInputs = with pkgs; [ + awscli2 + yubikey-manager + yq-go + jq + ]; + text = '' + set -euo pipefail + + CONFIG_DIR="$HOME/.config/aws-cli-mfa" + CONFIG_FILE="$CONFIG_DIR/config.yaml" + + usage() { + echo "Usage: $0 [--unset] [--help]" + echo "" + echo "Options:" + echo " --unset Unset AWS environment variables" + echo " --help Show this help message" + echo "" + echo "This tool assumes temporary credentials with MFA using a YubiKey." + echo "Configuration file: $CONFIG_FILE" + } + + unset_vars() { + if [[ -t 1 ]]; then + # Running directly - can't unset in parent shell, show instructions + echo "To unset AWS environment variables, run:" + echo " eval \$(aws-cli-mfa --unset)" + else + # Running from eval - output unset commands + echo "AWS environment variables unset." >&2 + cat << EOF +unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN +EOF + fi + } + + # Parse command line arguments + if [[ $# -gt 0 ]]; then + case "$1" in + --unset) + unset_vars + exit 0 + ;; + --help) + usage + exit 1 + ;; + *) + echo "Unknown option: $1" + usage + exit 1 + ;; + esac + fi + + # Check if config file exists + if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: Configuration file not found: $CONFIG_FILE" + echo "" + echo "Please create a configuration file with the following format:" + echo "" + echo "mfa_serial: arn:aws:iam::123456789012:mfa/username" + echo "role_arn: arn:aws:iam::123456789012:role/RoleName" + echo "session_duration: 43200 # optional, defaults to 43200 seconds (12 hours)" + echo "" + exit 1 + fi + + # Read configuration + MFA_SERIAL=$(yq eval '.mfa_serial' "$CONFIG_FILE") + ROLE_ARN=$(yq eval '.role_arn' "$CONFIG_FILE") + SESSION_DURATION=$(yq eval '.session_duration // 43200' "$CONFIG_FILE") + + if [[ "$MFA_SERIAL" == "null" || "$ROLE_ARN" == "null" ]]; then + echo "Error: mfa_serial and role_arn must be specified in $CONFIG_FILE" + exit 1 + fi + + echo "Getting TOTP code from YubiKey for device: $MFA_SERIAL" >&2 + + # Get TOTP code from YubiKey + if ! TOTP_CODE=$(ykman oath accounts code "$MFA_SERIAL" -s); then + echo "Error: Failed to get TOTP code from YubiKey for device '$MFA_SERIAL'" + echo "Available OATH accounts:" + ykman oath accounts list || echo "No OATH accounts found on YubiKey" + exit 1 + fi + + echo "Assuming role with MFA..." >&2 + + # Call AWS STS assume-role + if ! STS_RESPONSE=$(aws sts assume-role \ + --role-arn "$ROLE_ARN" \ + --role-session-name "aws-cli-mfa-$(date +%s)" \ + --serial-number "$MFA_SERIAL" \ + --token-code "$TOTP_CODE" \ + --duration-seconds "$SESSION_DURATION" \ + --output json); then + echo "Error: Failed to assume role. Check your AWS credentials and configuration." + exit 1 + fi + + # Extract credentials from response + AWS_ACCESS_KEY_ID=$(echo "$STS_RESPONSE" | jq -r '.Credentials.AccessKeyId') + AWS_SECRET_ACCESS_KEY=$(echo "$STS_RESPONSE" | jq -r '.Credentials.SecretAccessKey') + AWS_SESSION_TOKEN=$(echo "$STS_RESPONSE" | jq -r '.Credentials.SessionToken') + EXPIRATION=$(echo "$STS_RESPONSE" | jq -r '.Credentials.Expiration') + + if [[ "$AWS_ACCESS_KEY_ID" == "null" || "$AWS_SECRET_ACCESS_KEY" == "null" || "$AWS_SESSION_TOKEN" == "null" ]]; then + echo "Error: Failed to extract credentials from AWS response" + exit 1 + fi + + # Check if running from eval (stdout is a pipe) or directly + if [[ -t 1 ]]; then + # Running directly - can't export to parent shell, show instructions + echo "Success! AWS temporary credentials retrieved." + echo "Credentials expire at: $EXPIRATION" + echo "" + echo "To use these credentials in your current shell, run:" + echo " eval \$(aws-cli-mfa)" + echo "" + echo "To unset the credentials later, run:" + echo " eval \$(aws-cli-mfa --unset)" + else + # Running from eval - only output export commands + echo "Success! AWS temporary credentials have been set." >&2 + echo "Credentials expire at: $EXPIRATION" >&2 + echo "" >&2 + echo "To unset the credentials later, run:" >&2 + echo " eval \$(aws-cli-mfa --unset)" >&2 + + # Output export commands for eval + cat << EOF +export AWS_ACCESS_KEY_ID='$AWS_ACCESS_KEY_ID' +export AWS_SECRET_ACCESS_KEY='$AWS_SECRET_ACCESS_KEY' +export AWS_SESSION_TOKEN='$AWS_SESSION_TOKEN' +export AWS_SECURITY_TOKEN='$AWS_SESSION_TOKEN' +EOF + fi + ''; +} \ No newline at end of file diff --git a/secrets.yaml b/secrets.yaml index 1b3b4a6..a2968f6 100644 --- a/secrets.yaml +++ b/secrets.yaml @@ -2,6 +2,9 @@ private_keys: jmug: ENC[AES256_GCM,data:OOokznYtow9Ra0FRwBydclS6Z7w7mV9RTtHpjyw8NT+rV2VzTm/K8qYSw9N3gDbao5JOZH+LlVIL0UX2ZqGk8j/4D67LP+FmTUiIqyHH3LI8RcXAixIbR+zwV19Gr7DiFWw8jCyXQFFBJJRIatZLA5T2r23pYxL5drpQUcWsttuHRN7RwfXq2eFQogufzgfUqQgMkqvx64prDbrNq7bqZpPZE/vrCIoSK4rgTZCokD2hoIdEdzxLaVlbaoMMY7Dg83ojFU2Fpe5TWuS+duxUYjCPh86uoa3d0dsKoVkB9zd56EO5z0SB94tht9WJzmqKPh2gZ6DvV+wdaMH7wSqnUB6c2mE3i886Y4LQJjUFzpu1dnZaEEvJ0HI5jelZ3QVwjhp8/Zrkl/MwOG+lCHcIu2T7nQ/O0NxBNgMYbEYHGDcqi+anAzw2eNfep//dC+lT0qiXYAlzdDIIcTE7sL40qq7xXWtZJR4I6B+EOcYVn+zu93tsLiNke26h9ZoiAk40oxNZaiTPKnLkSvPtVZcoA53PamVKTS+tkXd+Pzt07lUgumTRIybkjE7i0hrhBrAv98NqvHobWfJVsQaH,iv:CDOXxoNTTw0rTsQbyYcXk0xL1UZtSbC0EG0audUwriM=,tag:XN2sujg9YHc5FtwCGPweSQ==,type:str] matcha: ENC[AES256_GCM,data:ySty7UiMQirmXmTf0unJuWX731NKKHH0lJQUxl4LlOHixm+1I13SNF7g+G9XYQzkvX/ijhzkDGLwIERVq1Di1UJsEw5w2YxeSitrzoJT0NZx+/Ip8DdgD/k+KnWgm7NeswcUTlt5cQbw7/IPkmRZ3qOV9UueMhH09AYEaSLpiKBUKQuMP/u2YgklwgWvVxW6syMPiYWmSnEfP3MfnkUbhhCuexdb2a/26BfVBbZeuLgBi25dxvRlykR6/uijCk3lHl84xPgWGKyR/130aHC2wp3arVjWO8/p2h2dobtnEnnDCWz2t+3CD1Qc3kdJcSNXhlFJWESOxzJp2KZFIKc3tF4emRSJ+EwP9byavrRozkHCBx4qCvLfdBzndJalR6ebh3jMyQIdRNBPG4x7+nSNnWZeK2uXhHg+m18CJfq2nKgVrpB4x+i48LyEwv907hFGLhfB4o+gfHxQtJYNoP3F0HX2xYndIjk6ihzbUi4C4YG7eUOF1IQ174FshFiadLfqAS+yh8oDUpKVthRu8kda6pPCtMI2jugIYzFrA30k6RWSj6QIsV8FE/jUdw++Htw9dutJSVsAncqCy4mZvqTIh0Cubnp9seBHX0l7vX0kAznoUMTK07lHBbkyeVlZHvKA+y34+SJVLpuS1eVjNhkeNpKkWAxwkoc/84ozpM2078bzy48rvMJBYIzjgqnqREnSs35Q8bQcHB32R0T6JEPoT7S8Z1GS7QB7kv8ulbA2H11Z,iv:8EBPvh7dpv23NtgwUmLn+2m/CKI6dZq72AXvB1OOdlc=,tag:1RCXZDcLOUP+hznVRgzMuA==,type:str] ace: ENC[AES256_GCM,data:rqhEzNlR978ZS+OppyHOWjYTfRKifRMXpqQfKS48oHkbcq7fQF6QtBBz3Ad4yFE3UJECcPipWqi5dhAMDVmOZT9gxEvEdhf6T7ecPHuQo95eUGPC46ZjoEwv7bykNBcncZFd4EHU2szuyqWb+xpGwJtnKqxDzi0ygsS6z/8ySlJWJ+LqCWrwsnERIblW5vgCvrT5Q2zkz3mR8pDNFc4Kh7B27SCBFG3EHST/NsEMhW0hU2eoIK5ppvHaKhVTx22ov2dP+nNbAhMi1WqHCGyQC2X4jeGGPXV8qXvev3Dl0s/VTCUsyqIIU5hPggWo++/XEspaoiLX7BsGklcfL5qGbF6YmSHYDBdgs5VOTzRUsepegmF79qXKO2XRgksH0he3aUOkpv9TVsmBIyPWqClOtA/QiQQ12gLCvqmBLcj66prvRS9qsZ++wxIINBFWhZh8F24pUHf6qpLpXmiEz9l7n1WocJjVgsI0hebYNN7sLtX/2IxVhPjNHVJQPPkDdLuw2841QKdO8cyDjCho3gbm1+RRDeyZWFSnnQUnKA+p2rE9ahtIJtUTuri7egGroJxPRWwgx2r3FW1Cm1ScdcL2W2AJQtnlJNqRnK/jXHXU3QHBkSiLTSwDLXG/0FV23/4Q+wCYp7ivBbztywT2Ngs8IMrFXz/6a7oGPaRqeendaVLJwrpkjruWv/nTdw0/PRchkLFuuP9ncJYVYaI2X6WwE9eiCAyG99M1X587w7SQe7cx,iv:HHfrC8PMHQS96YAzsyu7u52josTWNpgGa+qdjTKk7mk=,tag:9njC2670XZBUusf3cIv+gg==,type:str] +aws: + role_arn: ENC[AES256_GCM,data:YlYtqpsiTgHayuCFxY3pKfh5aBjNPf0UMGCoR+mFBUxe1CIU/Nkm+gzAOzwI,iv:Oo8d5y2g3lIVhrQgBT80PSxnZC0qXdqrumx76V1dz6w=,tag:gGJLjCYgcR3nHGhEbEpIGw==,type:str] + mfa_serial: ENC[AES256_GCM,data:O1Tzop8mjG48Hw19tAVoTvQ/z4UR3MvXzyEZZ8RArp73E7GsrR6jtsXnbrtNrigTgA==,iv:HumTqCU69e7zAUMsTn4lKr1WY8hU/SS/p5N2IXPTVWY=,tag:ATfjwZGSDmCaV7Zuoe9XzA==,type:str] yubico: u2f_keys: jmug: ENC[AES256_GCM,data:Z42zNo1DaQutPIfE+0PEAK5F1fmspJp6jmosHSHsUN6dSG4zY93Tdmvisxg0hFUbuMlYg/06z3bsagFY4q+9Eg6qCLqzj1Uzs3VA0vEP+N0UlR5YZvneWzhnw2KYaPSJ/dsxt9tSfJO89P5ffeJgfSds2hLRWngm0agkmZ1P9lRbY0iMTUGl9se4V/anydwH69GQLyul5EtXHr9KZyU2pkT86zQSHGqiiMm85TfyixTWi/PWFl1jtDlyUbvN2HZYFGdQ6O0E,iv:TYel/hCVAMQL1rqok/1YMqcGFuXmsvkwUcA988VULW8=,tag:dnPQiY5i3oHbsC9zdXvY4w==,type:str] @@ -44,7 +47,7 @@ sops: UjlDQ0Y5QnY4dmlVVFZrM0IyZzlISWcKwpQY9/f1O2v78/9/dCZ7HPE3wVwQ4COG a0E+oMEgBIeQny9LyfhUW2V/HKhYhFNPJaZrNM4J1zL+bz2ucdErmw== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-06-19T19:52:49Z" - mac: ENC[AES256_GCM,data:Fz/3QEoAjJy4psanCwDvIjUFSVDaSK+/Qjyr2M2c7eetv+USDaP1CraXaTK/OwKQfDWdnPHUOqfW1Oj51XJSPPoRlyYjXJxODjVXZfHo/EwnbpJs/81Lx66lYKljgCopFE6y2a7cFkM25g9aehyPhP/zdwULa/EmXcnuIimH8A0=,iv:dniso33D2uf4YUUbbODsbfm2k6dZdWdBTBPvnXTHL34=,tag:89KOctxIJ+lT7xUQR0qetA==,type:str] + lastmodified: "2025-07-15T00:54:12Z" + mac: ENC[AES256_GCM,data:KYcoP9vtmLRDaYVUfGtRy5XfHVgwuF6DxV2U2ZCQZ0oUTJRXj9eSjWJFRXlT243GQ11SgoUAcGFhATAOyqla3jsXuDiTn5oMydEwgSjVxjFpQf2RujCCqekk/7rcrRC7hVlUryNFylYy9wA9TWkGNGpaB/Z/a+Oaa5naDXODRAE=,iv:a6YVHIp4dIGxCIDmwM8r2AlBO40gptiLeNoyhRdJ+m8=,tag:DKsGjalA69F1ups/2sO5og==,type:str] unencrypted_suffix: _unencrypted version: 3.10.2