Skip to content

Instantly share code, notes, and snippets.

@renatoargh
Last active June 6, 2024 22:57
Show Gist options
  • Save renatoargh/3bc7ea01e5afefe6578464d18d5bfbcb to your computer and use it in GitHub Desktop.
Save renatoargh/3bc7ea01e5afefe6578464d18d5bfbcb to your computer and use it in GitHub Desktop.
Using temporary AWS STS credentials with AWS CLI and 1password

Summary

For those using 1password and AWS CLI, here is a cool trick to take advantage of 1pass and use temporary STS credentials instead of hardcoding long lived AWS creds, which are sensitive, on your /.aws/credentials file.

If MFA is enforced on AWS, this script will be very convenient once only STS credentials are accepted in this case.

Requirements

  1. Node.js installed
  2. AWS CLI installed
  3. 1password CLI installed

Usage

  1. Run npm install --global zx
  2. Paste both files above in your $HOME/.aws folder
  3. Run sudo chmod +x ~/.aws/resolve-credentials.mjs

Every time you need new credentials you just run ~/.aws/resolve-credentials.mjs personal.

PRO-TIP: Add an alias to ~/.aws/resolve-credentials.mjs on your ~/.zprofile file, like this alias c="~/.aws/resolve-credentials.mjs" usage will then become simpler, like this: c personal

References

Retrieving 1password secrets by reference: https://developer.1password.com/docs/cli/secret-references/#:~:text=Open%20and%20unlock%20the%201Password,then%20click%20Copy%20Secret%20Reference

# Take these values from 1password as explained here:
# https://developer.1password.com/docs/cli/secret-references/#:~:text=Open%20and%20unlock%20the%201Password,then%20click%20Copy%20Secret%20Reference.
{
"access_key_id": "op://Personal/AWS Personal Account/access key",
"secret_access_key": "op://Personal/AWS Personal Account/secret access key",
"mfa_device_identifier": "op://Personal/AWS Personal Account/MFA Device Identifier",
"mfa_token_code": "op://Personal/AWS Personal Account/one-time password",
"session_duration_seconds": 900,
"default_region": "us-east-1",
"default_output_format": "json"
}
#!/usr/bin/env zx
$.verbose = false;
import { writeFileSync } from 'fs';
import { extname } from 'path';
const profile = process.argv.length === 5
? process.argv[process.argv.length - 2]
: process.argv[process.argv.length - 1]
const mfaTokenCodeCandidate = process.argv[process.argv.length - 1] || ''
const mfaTokenCode = mfaTokenCodeCandidate.match(/[0-9]{6}/)
? mfaTokenCodeCandidate
: null
const homePath = process.env.HOME;
const awsFolderPath = `${homePath}/.aws`;
const credentialsFilePath = `${awsFolderPath}/credentials`;
if (extname(profile) === '.mjs') {
const availableConfigs =
(await $`ls ${awsFolderPath} | grep .json`)
.stdout
.trim()
.split('\n')
.map(f => f.replace('.json', ''))
.join(', ');
console.log('> Configuration missing.');
console.log('> Available configurations:', availableConfigs);
process.exit(1)
}
const rawCredentialRefs = await $`cat ${awsFolderPath}/${profile}.json`;
const credentialRefs = JSON.parse(rawCredentialRefs);
if (!mfaTokenCode && !credentialRefs.mfa_token_code_ref) {
console.log('Error: No mfa token code provided and no ref configured, failing')
process.exit(1)
}
await spinner(`> Obtaining temporary AWS credentials for "${profile}"`, async () => {
const sessionDurationSeconds = credentialRefs.session_duration_seconds;
const awsDefaultRegion = credentialRefs.default_region;
const awsDefaultOutputFormat = credentialRefs.default_output_format;
// To prevent multiple 1pass auth dialogs
const accessKeyId = credentialRefs.access_key_id || await $`op read ${credentialRefs.access_key_id_ref}`;
const [secretAccessKeyId, mfaDeviceId, tokenCode] = await Promise.all([
credentialRefs.secret_access_key || $`op read ${credentialRefs.secret_access_key_ref}`,
credentialRefs.mfa_device_identifier || $`op read ${credentialRefs.mfa_device_identifier_ref}`,
credentialRefs.mfa_token_code_ref ?
$`op read ${credentialRefs.mfa_token_code_ref}?attribute=otp` :
mfaTokenCode,
]);
const rawSessionTokenOutput = await $`
AWS_ACCESS_KEY_ID=${accessKeyId} \
AWS_SECRET_ACCESS_KEY=${secretAccessKeyId} \
AWS_DEFAULT_REGION=${awsDefaultRegion} \
aws sts get-session-token --output=json \
--serial-number ${mfaDeviceId} \
--token-code ${tokenCode} \
--duration-seconds ${sessionDurationSeconds}
`;
const { Credentials: credentials } = JSON.parse(rawSessionTokenOutput);
let credentialsFile = `#
# TEMPORARY CREDENTIALS FOR "${profile}" AWS ACCOUNT
# EXPIRES AT: ${new Date(credentials.Expiration).toLocaleString()}
# RUN \`${awsFolderPath}/resolve-credentials.mjs\ ${profile}\` TO REFRESH
# EDIT \`${awsFolderPath}/${profile}.json\` TO UPDATE CONFIGURATION
#
[default]
aws_access_key_id=${credentials.AccessKeyId}
aws_secret_access_key=${credentials.SecretAccessKey}
aws_session_token=${credentials.SessionToken}
region=${awsDefaultRegion}
output=${awsDefaultOutputFormat}
`;
Object.entries(credentialRefs.profiles || {})
.forEach(([profile, profileConfig]) => {
credentialsFile += `
[${profile}]
role_arn=${profileConfig.roleArn}
source_profile=${profileConfig.sourceProfile || 'default'}
`
})
writeFileSync(credentialsFilePath, credentialsFile.trim());
})
const formatObject = (data) => {
if (Array.isArray(data)) {
return data.
map(value => `\n#\t - ${formatObject(value)}`)
.join('\n');
}
if (typeof data === 'object') {
return Object.entries(data)
.map(([key, value]) => `# ${key}: ${formatObject(value)}`)
.join('\n');
}
if (!data) {
return '';
}
return data.toString()
}
const COMMENT_SEPARATOR = '\n#\n'
const encloseWith = (text, string) =>
`${string}${text}${string}`
await spinner('> Obtaining caller identity and account aliases', async () => {
const [
callerIdentityResponse,
accountAliasesResponse
] = await Promise.allSettled([
$`aws sts get-caller-identity --output=json`,
$`aws iam list-account-aliases --output=json`,
// aws organizations describe-account --account-id 537397127806
])
let extraInfo = []
if (callerIdentityResponse.status === 'fulfilled') {
const { stdout: callerIdentity } = callerIdentityResponse.value
extraInfo.push(formatObject(JSON.parse(callerIdentity)));
}
if (accountAliasesResponse.status === 'fulfilled') {
const { stdout: accountAliases } = accountAliasesResponse.value
extraInfo.push(formatObject(JSON.parse(accountAliases)));
}
if (extraInfo) {
extraInfo = extraInfo.join(COMMENT_SEPARATOR);
extraInfo = '\n\n' + encloseWith(extraInfo, COMMENT_SEPARATOR).trim();
}
await $`echo ${extraInfo} >> ${credentialsFilePath}`
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment