Remove AWS credentials file for secure coding agent use

Detailed guide how to use 1Password CLI (op) for AWS credentials so malicious skills cannot extract AWS credentials easily

There is no 100% security, this does not guarantee that attackers cannot steal your AWS credentials. It only reduces the most obvious attack vectors!

Couple of weeks ago we rolled out Codex for every developer at esome. I have been a heavy user since beginning of the year and it was only natural to do a Q&A session with other heavy users and for those who only got introduced to the style of LLM usage.

What I did not expect: Most of the questions were about security.

During the meeting I mostly answered with “did not care about it, used yolo mode all the time”. But it got me thinking for a solution and I think I found one for my workflow, which at least covers the most obvious attack vectors.

The problem

By default, AWS credentials are stored in plain text in ~/.aws/credentials file. As long as Codex/Claude Code is allowed to execute arbitrary bash commands, a small malicious text in a skill you install could lead to a command executed that reads the content of the file and POST-ing it somewhere with curl. Couple of seconds, credentials gone.

Depending if you were able to spot the command being executed, bad actors could do all kind of things depending on the permissions your AWS user has.

Therefore we want to:

The solution

There are several options for not storing AWS credentials in plain text. All of them revolve somehow about a keychain-like secure backend, depending on your OS. I started with aws-vault which uses macOS keychain, but quickly got annoyed due to buggy/ not working touch ID. And no way I am going to enter my keychain password manually every time my agent wants to access kubectl for debugging an issue. I will still attach relevant scripts for aws-vault below.

Since I use 1Password for my private stuff, I checked it out and they have a first-party plugin for working with AWS CLI.

op (1Password CLI)

kubectl

When accessing k8s via AWS, you need to update your ~/.kube/config to request credentials from op secret storage

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: xxx
    server: https://xxx.eu-central-1.eks.amazonaws.com
  name: arn:aws:eks:eu-central-1:xxx:xxx/xxx
contexts:
- context:
    cluster: arn:aws:eks:eu-central-1:xxx:xxx/xxx
    user: arn:aws:eks:eu-central-1:xxx:xxx/xxx
  name: xxx
current-context: xxx
kind: Config
users:
- name: arn:aws:eks:eu-central-1:xxx:xxx/xxx
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      # this is the relevant section you have to update in your config
      - -lc
      - exec op plugin run -- aws --region eu-central-1 eks get-token --cluster-name eks01-prd --output json
      command: zsh
      env:
      - name: AWS_PROFILE
        value: prd
      interactiveMode: IfAvailable
      provideClusterInfo: false

Application credentials (Node.js)

If your locally running service you are developing needs access to AWS resources, e.g. AWS Bedrock, this whole thing might break apart. Your application does not know about a shell or the created alias from 1Password.

In these cases you can use 1Password environments.

import { fromNodeProviderChain, fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import type { AwsCredentialIdentity, Provider } from '@aws-sdk/types';

interface AwsCredentialOptions {
    accessKeyId?: string;
    secretAccessKey?: string;
    sessionToken?: string;
    profile?: string;
    region?: string;
    roleArn?: string;
    roleSessionName?: string;
}

// Local static env credentials need to assume the DevOps role before calling Bedrock.
const buildAssumeRoleCredentialProvider = ({
    masterCredentials,
    region,
    roleArn,
    roleSessionName,
}: {
    masterCredentials: AwsCredentialIdentity | Provider<AwsCredentialIdentity>;
    region?: string;
    roleArn: string;
    roleSessionName?: string;
}): Provider<AwsCredentialIdentity> =>
    fromTemporaryCredentials({
        masterCredentials,
        params: {
            RoleArn: roleArn,
            RoleSessionName: roleSessionName ?? 'xxx',
        },
        clientConfig: region ? { region } : undefined,
    });

const buildStaticCredentials = ({
    accessKeyId,
    secretAccessKey,
    sessionToken,
}: Required<Pick<AwsCredentialOptions, 'accessKeyId' | 'secretAccessKey'>> &
    Pick<AwsCredentialOptions, 'sessionToken'>): AwsCredentialIdentity => ({
    accessKeyId,
    secretAccessKey,
    ...(sessionToken ? { sessionToken } : {}),
});

export const buildAwsCredentialProvider = ({
    accessKeyId,
    secretAccessKey,
    sessionToken,
    profile,
    region,
    roleArn,
    roleSessionName,
}: AwsCredentialOptions = {}): Provider<AwsCredentialIdentity> | undefined => {
    if (accessKeyId && secretAccessKey) {
        const credentials = buildStaticCredentials({
            accessKeyId,
            secretAccessKey,
            sessionToken,
        });

        if (roleArn) {
            return buildAssumeRoleCredentialProvider({
                masterCredentials: credentials,
                region,
                roleArn,
                roleSessionName,
            });
        }

        return async () => credentials;
    }

    if (profile) {
        return fromNodeProviderChain({ profile });
    }

    return fromNodeProviderChain();
};

export const buildAwsClientCredentials = buildAwsCredentialProvider;

aws-vault

kubectl

When accessing k8s via AWS, you need to update your ~/.kube/config to request credentials from aws-vault

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: xxx
    server: https://xxx.eu-central-1.eks.amazonaws.com
  name: arn:aws:eks:eu-central-1:xxx:xxx/xxx
contexts:
- context:
    cluster: arn:aws:eks:eu-central-1:xxx:xxx/xxx
    user: arn:aws:eks:eu-central-1:xxx:xxx/xxx
  name: xxx
current-context: xxx
kind: Config
users:
- name: arn:aws:eks:eu-central-1:xxx:xxx/xxx
  user:
   exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      # this is the relevant section you have to update in your config
      args:
      - exec
      - prd
      - --
      - aws
      - --region
      - eu-central-1
      - eks
      - get-token
      - --cluster-name
      - xxx
      - --output
      - json
      command: aws-vault
      env:
      - name: AWS_PROFILE
        value: prd
      interactiveMode: IfAvailable
      provideClusterInfo: false

Application Credentials (Node.js)

There appears to be a way to integrate Docker with aws-vault, but the setup looks rather complex, and I haven’t tried it yet: https://github.com/ByteNess/aws-vault/blob/main/USAGE.md#docker