# Ersätta AWS Access Keys med IAM Roles Anywhere: En certifikatbaserad tutorial

- Författare: [Jonas Odencrants](https://paperphyte.com/konsulter/jonas-odencrants), [Paperphyte](https://paperphyte.com)
- Publicerad: 2026-06-18
- Kategori: AWS
- Etiketter: AWS, IAM Roles Anywhere, Säkerhet, Terraform, OpenSSL, Certifikat
- Källa: <https://paperphyte.com/anteckningsbok/ersatta-aws-access-keys-med-iam-roles-anywhere>

En praktisk genomgång av hur du ersätter långlivade AWS access keys med certifikatbaserad autentisering via IAM Roles Anywhere – från CA-certifikat och klientcertifikat till Terraform-resurser och aws_signing_helper.

Jag arbetade nyligen med en migrering där robot- och serviceanvändare i AWS flyttades bort från långlivade `AWS_ACCESS_KEY_ID` och `AWS_SECRET_ACCESS_KEY` och över till [roles-anywhere](https://aws.amazon.com/iam/roles-anywhere/).

Målet var enkelt: ersätta statisk nyckelbaserad åtkomst med certifikatbaserad autentisering.

I stället för att ge en serviceanvändare permanenta AWS-åtkomstnycklar autentiserar workloaden med utfärdade X.509-certifikat. Dessa certifikat signeras av en certifikatutfärdare, och AWS IAM Roles Anywhere använder den betrodda certifikatkedjan för att utfärda temporära AWS-credentials.

I den här guiden går vi igenom hela uppsättningen:

1. Generera ett certifikat för en certificate authority.
2. Spara authority certificate bundle i AWS Secrets Manager.
3. Utfärda klientcertifikat för workloads.
4. Konfigurera de Terraform-resurser som krävs:
    - IAM Roles Anywhere trust anchor
    - IAM Roles Anywhere profile
    - IAM role
    - S3 access policy
5. Konfigurera `aws_signing_helper` så att lokala verktyg kan autentisera mot AWS med certifikat.

## Så hänger lösningen ihop

På en övergripande nivå ser lösningen ut så här:

- Ett root- eller authority-certifikat genereras.
- Authority-certifikatet konfigureras som en trust anchor i AWS IAM Roles Anywhere.
- Klientcertifikat utfärdas och signeras av authority-certifikatet.
- En workload använder `aws_signing_helper` med klientcertifikatet och den privata nyckeln.
- AWS validerar certifikatkedjan och returnerar temporära credentials för den konfigurerade IAM-rollen.

## Skapa authority-certifikatet

Det första scriptet skapar authority-certifikatet. Det är beroende av `generate-config-template.sh`, som bygger OpenSSL-konfigurationen som används när certifikatet genereras.

Scriptet förutsätter också att användaren som kör det redan är autentiserad mot AWS, eftersom det skriver den genererade trust anchor certificate bundle till AWS Secrets Manager.

**generate-certificate.sh**

```bash
#!/bin/bash
#
# generate-certificate.sh
#
# Generates a self-signed X.509v3 CA certificate for use as an IAM Roles
# Anywhere trust anchor,
#
# The CA certificate is x.509 v3, uses an EC key (secp384r1 by default) and a
# SHA-384 signature, with basicConstraints CA:TRUE and keyUsage Digital
# Signature + Certificate Sign + CRL Sign. It builds the OpenSSL config via
# generate-config-template.sh, then creates a private key and certificate.
#
# Usage:
#   ./generate-certificate.sh \
#  --server test-reports.roles-anywhere.internal \
#  --name test-reports \
#  --out-dir ./ca \
#  --subject "/C=SE/ST=Stockholm/O=Paperphyte /CN=roles-anywhere" \
#  --keep-config, and -- to forward any config-template flags.
# Run with -h/--help for the full list of options.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_GENERATOR="${SCRIPT_DIR}/generate-config-template.sh"

# --- Defaults --------------------------------------------------------------
SERVER=""
OUT_DIR="."
NAME="ca"
DAYS="3650"
CURVE="secp384r1"
DIGEST="sha384"
SUBJECT=""               # e.g. /C=SE/ST=Stockholm/O=Paperphyte/CN=roles-anywhere
KEEP_CONFIG="false"

# Anything after `--` is forwarded verbatim to generate-config-template.sh
PASSTHROUGH=()

# --- Help ------------------------------------------------------------------
usage() {
  cat <<'USAGE'
Generate a self-signed X.509v3 CA certificate (CA:TRUE) for an IAM Roles Anywhere trust anchor.

Options:
  -s, --server NAME      subjectAltName DNS entry (required)
  -o, --out-dir DIR      Output directory (default: .)
  -n, --name NAME        Base name for generated files (default: ca)
                         -> <name>.key, <name>.crt, <name>.cnf
  -d, --days N           Certificate validity in days (default: 3650)
  -c, --curve NAME       EC curve for the private key (default: secp384r1)
      --digest NAME      Signing digest, SHA256 or stronger (default: sha384)
      --subject DN       OpenSSL subject string, e.g.
                         /C=SE/ST=Stockholm/O=Paperphyte/CN=roles-anywhere
                         (if omitted, openssl prompts interactively)
      --keep-config      Keep the generated .cnf file (default: removed)
  -h, --help             Show this help and exit

  --                     Forward all following args to generate-config-template.sh
                         e.g. -- --organization-name "Paperphyte AB" --common-name-max 128

Outputs (in --out-dir):
  <name>.key   Private key (mode 600)
  <name>.crt   Self-signed X.509v3 CA certificate (CA:TRUE)
  <name>.pem   Trust anchor certificate bundle (chain, root CA at the top)
USAGE
}

# --- Arg parsing -----------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    -s|--server)    SERVER="$2"; shift 2 ;;
    -o|--out-dir)   OUT_DIR="$2"; shift 2 ;;
    -n|--name)      NAME="$2"; shift 2 ;;
    -d|--days)      DAYS="$2"; shift 2 ;;
    -c|--curve)     CURVE="$2"; shift 2 ;;
    --digest)       DIGEST="$2"; shift 2 ;;
    --subject)      SUBJECT="$2"; shift 2 ;;
    --keep-config)  KEEP_CONFIG="true"; shift ;;
    -h|--help)      usage; exit 0 ;;
    --)             shift; PASSTHROUGH=("$@"); break ;;
    *)              echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
  esac
done

if [[ -z "$SERVER" ]]; then
  echo "Error: --server is required." >&2
  exit 1
fi

# Roles Anywhere requires SHA256 or stronger; reject weaker digests.
case "${DIGEST,,}" in
  sha256|sha384|sha512|sha3-256|sha3-384|sha3-512) ;;
  *) echo "Error: --digest must be SHA256 or stronger (got '${DIGEST}')." >&2; exit 1 ;;
esac

command -v openssl >/dev/null 2>&1 || { echo "Error: openssl not found in PATH." >&2; exit 1; }
[[ -x "$CONFIG_GENERATOR" ]] || { echo "Error: cannot execute ${CONFIG_GENERATOR}." >&2; exit 1; }

mkdir -p "$OUT_DIR"
CONFIG_FILE="${OUT_DIR}/${NAME}.cnf"
KEY_FILE="${OUT_DIR}/${NAME}.key"
CERT_FILE="${OUT_DIR}/${NAME}.crt"
# Trust anchor certificate bundle: the full chain with the root CA at the top.
# For this self-signed root CA the chain is just the root certificate itself.
BUNDLE_FILE="${OUT_DIR}/${NAME}.pem"
# --- Build the OpenSSL config ---------------------------------------------
echo "==> Generating OpenSSL config"
"$CONFIG_GENERATOR" --server "$SERVER" --output "$CONFIG_FILE" "${PASSTHROUGH[@]}"

cleanup_config() {
  if [[ "$KEEP_CONFIG" != "true" ]]; then
    rm -f "$CONFIG_FILE"
  fi
}
trap cleanup_config EXIT

# --- Private key -----------------------------------------------------------
echo "==> Generating EC private key (${CURVE}): ${KEY_FILE}"
# Use genpkey (PKCS#8, single PEM block). `openssl ecparam -genkey` prepends an
# EC PARAMETERS block that the Roles Anywhere credential helper cannot parse.
openssl genpkey -algorithm EC -pkeyopt "ec_paramgen_curve:${CURVE}" -out "$KEY_FILE"
chmod 600 "$KEY_FILE"

# --- Self-signed CA certificate -------------------------------------------
echo "==> Generating self-signed CA certificate (CA:TRUE): ${CERT_FILE}"
subject_args=()
if [[ -n "$SUBJECT" ]]; then
  subject_args=(-subj "$SUBJECT")
fi

openssl req -new -x509 \
  -"$DIGEST" \
  -config "$CONFIG_FILE" \
  -extensions v3_ca \
  -key "$KEY_FILE" \
  -out "$CERT_FILE" \
  -days "$DAYS" \
  "${subject_args[@]}"

# --- Trust anchor bundle ---------------------------------------------------
# Build the certificate chain bundle (root CA at the top) that gets configured
# as the IAM Roles Anywhere trust anchor. A self-signed root CA is its own
# chain, so the bundle contains only the root certificate.
echo "==> Building trust anchor bundle: ${BUNDLE_FILE}"
openssl x509 -in "$CERT_FILE" -out "$BUNDLE_FILE" -outform PEM

echo
echo "==> Done"
echo "    Private key  : ${KEY_FILE}"
echo "    Certificate  : ${CERT_FILE}"
echo "    Trust bundle : ${BUNDLE_FILE}"
[[ "$KEEP_CONFIG" == "true" ]] && echo "    Config       : ${CONFIG_FILE}"
echo
echo "Certificate summary:"
openssl x509 -in "$CERT_FILE" -noout -subject -issuer -dates

# Store the trust anchor bundle (the chain configured on the trust anchor).
SECRET_ID="/roles/anywhere/${NAME}"
if aws secretsmanager describe-secret --secret-id "$SECRET_ID" >/dev/null 2>&1; then
    echo "==> Secret ${SECRET_ID} exists, updating value"
    aws secretsmanager put-secret-value --secret-id "$SECRET_ID" \
        --secret-string file://$BUNDLE_FILE
else
    echo "==> Creating secret ${SECRET_ID}"
    aws secretsmanager create-secret --name "$SECRET_ID" \
        --secret-string file://$BUNDLE_FILE \
        --tags '[{"Key": "squad", "Value": "satin"},{"Key": "env", "Value": "prod"},{"Key": "service", "Value": "common-infra"}]'
fi
```

Hjälpscriptet nedan genererar OpenSSL-konfigurationsfilen som används av scriptet för authority-certifikatet.

Det är användbart eftersom certifikatfält, SAN-värde, constraints och key usage-inställningar definieras på ett och samma ställe. Genom att generera konfigurationen dynamiskt blir certifikatgenereringen repeterbar och enklare att anpassa för olika workloads eller miljöer.

**generate-config-template.sh**

```bash
#!/bin/bash
#
# generate-config-template.sh
#
# Generates an OpenSSL config (req) template. Every field rendered into the
# config is configurable from the command line; sensible defaults are applied
# when a flag is omitted.
#
# Usage:
#   ./generate-config-template.sh --server my.host.example [options]
#
# Run with -h/--help for the full list of options.

set -euo pipefail

# --- Defaults --------------------------------------------------------------
OUTPUT="config.cnf"
SERVER=""

# req sections
DISTINGUISHED_NAME="req_distinguished_name"
ATTRIBUTES="req_attributes"

# req_distinguished_name prompts / constraints
COUNTRY_NAME="SE"
COUNTRY_NAME_MIN="2"
COUNTRY_NAME_MAX="2"
STATE_OR_PROVINCE_NAME="Stockholm"
LOCALITY_NAME="Tumba"
ORGANIZATION_NAME="Paperphyte AB"
ORGANIZATIONAL_UNIT_NAME="Satin"
COMMON_NAME="Common Name (eg, fully qualified host name)"
COMMON_NAME_MAX="64"
EMAIL_ADDRESS="Email Address"
EMAIL_ADDRESS_MAX="64"

# req_attributes
CHALLENGE_PASSWORD="A challenge password"
CHALLENGE_PASSWORD_MIN="4"
CHALLENGE_PASSWORD_MAX="20"

# v3_ca
BASIC_CONSTRAINTS="critical, CA:TRUE"
SUBJECT_KEY_IDENTIFIER="hash"
AUTHORITY_KEY_IDENTIFIER="keyid:always, issuer:always"
KEY_USAGE="critical, digitalSignature, keyCertSign, cRLSign"

# --- Help ------------------------------------------------------------------
usage() {
  cat <<'USAGE'
Generate an OpenSSL req config template.

Options (all optional except --server):
  -o, --output FILE                  Output file (default: config.cnf)
  -s, --server NAME                  Value for subjectAltName DNS entry (required)

  --distinguished-name NAME          [req] distinguished_name section name
  --attributes NAME                  [req] attributes section name

  --country-name TEXT                countryName prompt
  --country-name-min N               countryName_min
  --country-name-max N               countryName_max
  --state-name TEXT                  stateOrProvinceName prompt
  --locality-name TEXT               localityName prompt
  --organization-name TEXT           0.organizationName prompt
  --organizational-unit-name TEXT    organizationalUnitName prompt
  --common-name TEXT                 commonName prompt
  --common-name-max N                commonName_max
  --email-address TEXT               emailAddress prompt
  --email-address-max N              emailAddress_max

  --challenge-password TEXT          challengePassword prompt
  --challenge-password-min N         challengePassword_min
  --challenge-password-max N         challengePassword_max

  --basic-constraints TEXT           [v3_ca] basicConstraints
  --subject-key-identifier TEXT      [v3_ca] subjectKeyIdentifier
  --authority-key-identifier TEXT    [v3_ca] authorityKeyIdentifier
  --key-usage TEXT                   [v3_ca] keyUsage

  -h, --help                         Show this help and exit
USAGE
}

# --- Arg parsing -----------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    -o|--output)                    OUTPUT="$2"; shift 2 ;;
    -s|--server)                    SERVER="$2"; shift 2 ;;
    --distinguished-name)           DISTINGUISHED_NAME="$2"; shift 2 ;;
    --attributes)                   ATTRIBUTES="$2"; shift 2 ;;
    --country-name)                 COUNTRY_NAME="$2"; shift 2 ;;
    --country-name-min)             COUNTRY_NAME_MIN="$2"; shift 2 ;;
    --country-name-max)             COUNTRY_NAME_MAX="$2"; shift 2 ;;
    --state-name)                   STATE_OR_PROVINCE_NAME="$2"; shift 2 ;;
    --locality-name)                LOCALITY_NAME="$2"; shift 2 ;;
    --organization-name)            ORGANIZATION_NAME="$2"; shift 2 ;;
    --organizational-unit-name)     ORGANIZATIONAL_UNIT_NAME="$2"; shift 2 ;;
    --common-name)                  COMMON_NAME="$2"; shift 2 ;;
    --common-name-max)              COMMON_NAME_MAX="$2"; shift 2 ;;
    --email-address)                EMAIL_ADDRESS="$2"; shift 2 ;;
    --email-address-max)            EMAIL_ADDRESS_MAX="$2"; shift 2 ;;
    --challenge-password)           CHALLENGE_PASSWORD="$2"; shift 2 ;;
    --challenge-password-min)       CHALLENGE_PASSWORD_MIN="$2"; shift 2 ;;
    --challenge-password-max)       CHALLENGE_PASSWORD_MAX="$2"; shift 2 ;;
    --basic-constraints)            BASIC_CONSTRAINTS="$2"; shift 2 ;;
    --subject-key-identifier)       SUBJECT_KEY_IDENTIFIER="$2"; shift 2 ;;
    --authority-key-identifier)     AUTHORITY_KEY_IDENTIFIER="$2"; shift 2 ;;
    --key-usage)                    KEY_USAGE="$2"; shift 2 ;;
    -h|--help)                      usage; exit 0 ;;
    *)                              echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
  esac
done

if [[ -z "$SERVER" ]]; then
  echo "Error: --server is required (used for subjectAltName DNS entry)." >&2
  exit 1
fi

# --- Render ----------------------------------------------------------------
cat > "$OUTPUT" <<EOF
[ req ]
distinguished_name  = ${DISTINGUISHED_NAME}
attributes      = ${ATTRIBUTES}

[ ${DISTINGUISHED_NAME} ]
countryName         = ${COUNTRY_NAME}
countryName_min         = ${COUNTRY_NAME_MIN}
countryName_max         = ${COUNTRY_NAME_MAX}
stateOrProvinceName     = ${STATE_OR_PROVINCE_NAME}
localityName            = ${LOCALITY_NAME}
0.organizationName      = ${ORGANIZATION_NAME}
organizationalUnitName      = ${ORGANIZATIONAL_UNIT_NAME}
commonName          = ${COMMON_NAME}
commonName_max          = ${COMMON_NAME_MAX}
emailAddress            = ${EMAIL_ADDRESS}
emailAddress_max        = ${EMAIL_ADDRESS_MAX}

[ ${ATTRIBUTES} ]
challengePassword       = ${CHALLENGE_PASSWORD}
challengePassword_min       = ${CHALLENGE_PASSWORD_MIN}
challengePassword_max       = ${CHALLENGE_PASSWORD_MAX}

[ v3_ca ]
basicConstraints        = ${BASIC_CONSTRAINTS}
subjectKeyIdentifier    = ${SUBJECT_KEY_IDENTIFIER}
authorityKeyIdentifier  = ${AUTHORITY_KEY_IDENTIFIER}
keyUsage                = ${KEY_USAGE}

[SAN]
subjectAltName=DNS:${SERVER}
EOF

echo "Wrote ${OUTPUT} (subjectAltName DNS:${SERVER})"
```

## Utfärda klientcertifikat

När authority-certifikatet finns på plats är nästa steg att utfärda klientcertifikat.

Ett klientcertifikat är det certifikat som en workload eller lokal maskin använder när den autentiserar mot AWS via IAM Roles Anywhere. Det viktiga är att klientcertifikatet måste vara signerat av det authority-certifikat som AWS litar på via trust anchor.

Det här scriptet genererar en privat nyckel, skapar en certificate signing request, signerar requesten med authority-certifikatet och skriver det resulterande klientcertifikatet i PEM-format.

**issue-client-certificate.sh**

```bash
#!/bin/bash
#
# issue-client-certificate.sh
#
# Issues an X.509v3 client (end-entity) certificate signed by a CA, for use
# by a workload authenticating to IAM Roles Anywhere.
#
# The client certificate is x.509 v3, uses an EC key (secp384r1 by default)
# and a SHA-384 signature, with basicConstraints CA:FALSE and keyUsage
# Digital Signature. It is signed by the CA produced by generate-certificate.sh
# (the trust anchor).
#
# Usage:
#   ./issue-client-certificate.sh \
#     --ca-cert ./ca/test-reports/test-reports.crt \
#     --ca-key  ./ca/test-reports/test-reports.key \
#     --server  test-reports.roles-anywhere.internal \
#     --name    test-reports \
#     --out-dir ./clients \
#     --subject "/C=SE/ST=Stockholm/O=Paperphyte/CN=test-reports"
# Run with -h/--help for the full list of options.

set -euo pipefail

# --- Defaults --------------------------------------------------------------
CA_CERT=""
CA_KEY=""
SERVER=""
OUT_DIR="."
NAME="client"
DAYS="365"
CURVE="secp384r1"
DIGEST="sha384"
SUBJECT=""               # e.g. /C=SE/ST=Stockholm/O=Paperphyte/CN=my-workload

# --- Help ------------------------------------------------------------------
usage() {
  cat <<'USAGE'
Issue an X.509v3 client certificate (CA:FALSE) signed by a CA, for IAM Roles Anywhere.

Options:
      --ca-cert FILE     CA certificate that signs the client cert (required)
      --ca-key FILE      CA private key (required)
  -s, --server NAME      subjectAltName DNS entry (required)
  -o, --out-dir DIR      Output directory (default: .)
  -n, --name NAME        Base name for generated files (default: client)
                         -> <name>.key, <name>.crt, <name>.csr
  -d, --days N           Certificate validity in days (default: 365)
  -c, --curve NAME       EC curve for the private key (default: secp384r1)
      --digest NAME      Signing digest, SHA256 or stronger (default: sha384)
      --subject DN       OpenSSL subject string, e.g.
                         /C=SE/ST=Stockholm/O=Paperphyte/CN=my-workload
                         (if omitted, openssl prompts interactively)
  -h, --help             Show this help and exit

Outputs (in --out-dir):
  <name>.key   Private key (mode 600)
  <name>.crt   X.509v3 client certificate (CA:FALSE), signed by the CA
  <name>.pem   Leaf certificate in PEM (use as --certificate for the helper)
  <name>.csr   Certificate signing request
USAGE
}

# --- Arg parsing -----------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    --ca-cert)      CA_CERT="$2"; shift 2 ;;
    --ca-key)       CA_KEY="$2"; shift 2 ;;
    -s|--server)    SERVER="$2"; shift 2 ;;
    -o|--out-dir)   OUT_DIR="$2"; shift 2 ;;
    -n|--name)      NAME="$2"; shift 2 ;;
    -d|--days)      DAYS="$2"; shift 2 ;;
    -c|--curve)     CURVE="$2"; shift 2 ;;
    --digest)       DIGEST="$2"; shift 2 ;;
    --subject)      SUBJECT="$2"; shift 2 ;;
    -h|--help)      usage; exit 0 ;;
    *)              echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
  esac
done

for req in CA_CERT CA_KEY SERVER; do
  if [[ -z "${!req}" ]]; then
    echo "Error: --${req,,} is required." | tr '_' '-' >&2
    exit 1
  fi
done
[[ -r "$CA_CERT" ]] || { echo "Error: cannot read CA cert ${CA_CERT}." >&2; exit 1; }
[[ -r "$CA_KEY"  ]] || { echo "Error: cannot read CA key ${CA_KEY}." >&2; exit 1; }

# Roles Anywhere requires SHA256 or stronger; reject weaker digests.
case "${DIGEST,,}" in
  sha256|sha384|sha512|sha3-256|sha3-384|sha3-512) ;;
  *) echo "Error: --digest must be SHA256 or stronger (got '${DIGEST}')." >&2; exit 1 ;;
esac

command -v openssl >/dev/null 2>&1 || { echo "Error: openssl not found in PATH." >&2; exit 1; }

mkdir -p "$OUT_DIR"
KEY_FILE="${OUT_DIR}/${NAME}.key"
CSR_FILE="${OUT_DIR}/${NAME}.csr"
CERT_FILE="${OUT_DIR}/${NAME}.crt"
PEM_FILE="${OUT_DIR}/${NAME}.pem"
EXT_FILE="${OUT_DIR}/${NAME}.ext"

cleanup_ext() { rm -f "$EXT_FILE"; }
trap cleanup_ext EXIT

# --- Client extensions -----------------------------------------------------
# v3 end-entity profile: not a CA, key used to sign the Roles Anywhere session.
cat > "$EXT_FILE" <<EOF
basicConstraints       = critical, CA:FALSE
keyUsage               = critical, digitalSignature
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always, issuer:always
subjectAltName         = DNS:${SERVER}
EOF

# --- Private key -----------------------------------------------------------
echo "==> Generating EC private key (${CURVE}): ${KEY_FILE}"
# Use genpkey (PKCS#8, single PEM block). `openssl ecparam -genkey` prepends an
# EC PARAMETERS block that the Roles Anywhere credential helper cannot parse.
openssl genpkey -algorithm EC -pkeyopt "ec_paramgen_curve:${CURVE}" -out "$KEY_FILE"
chmod 600 "$KEY_FILE"

# --- Certificate signing request -------------------------------------------
echo "==> Generating CSR: ${CSR_FILE}"
subject_args=()
if [[ -n "$SUBJECT" ]]; then
  subject_args=(-subj "$SUBJECT")
fi
openssl req -new \
  -"$DIGEST" \
  -nodes \
  -key "$KEY_FILE" \
  -out "$CSR_FILE" \
  "${subject_args[@]}"

# --- Sign with the CA ------------------------------------------------------
echo "==> Signing client certificate with CA: ${CERT_FILE}"
openssl x509 -req \
  -"$DIGEST" \
  -days "$DAYS" \
  -in "$CSR_FILE" \
  -CA "$CA_CERT" \
  -CAkey "$CA_KEY" \
  -CAcreateserial \
  -extfile "$EXT_FILE" \
  -out "$CERT_FILE"

# Leaf certificate in PEM form, used as --certificate by the Roles Anywhere
# credential helper (aws_signing_helper).
openssl x509 -in "$CERT_FILE" -out "$PEM_FILE" -outform PEM

echo
echo "==> Done"
echo "    Private key : ${KEY_FILE}"
echo "    Certificate : ${CERT_FILE}"
echo "    PEM (leaf)  : ${PEM_FILE}"
echo "    CSR         : ${CSR_FILE}"
echo
echo "Certificate summary:"
openssl x509 -in "$CERT_FILE" -noout -subject -issuer -dates

echo
echo "Verifying chain against CA:"
openssl verify -CAfile "$CA_CERT" "$CERT_FILE"
```

## Trust anchor, profile och IAM-roll i Terraform

När certifikaten har genererats måste authority-certifikatet kopplas till en IAM Roles Anywhere trust anchor.

Trust anchorn talar om för AWS vilken certificate authority som ska vara betrodd. När trust anchorn finns på plats behöver du också en IAM Roles Anywhere profile och en IAM role som workloaden kan anta.

Terraform-koden nedan skapar de AWS-resurser som behövs för en workload som lagrar och hämtar test reports i en S3-bucket.

**main.tf**

```terraform
# Trust anchors
resource "aws_rolesanywhere_trust_anchor" "trust_anchor" {
  name    = "${var.bucket_name}-trust_anchor"
  enabled = true
  source {
    source_data {
      x509_certificate_data = data.aws_secretsmanager_secret_version.certificate_secret.secret_string
    }
    source_type = "CERTIFICATE_BUNDLE"
  }
}

resource "aws_rolesanywhere_profile" "profile" {
  name                = "${var.bucket_name}-profile"
  enabled             = true
  role_arns           = [aws_iam_role.role_anywhere.arn]
  managed_policy_arns = [aws_iam_policy.iamanywhere.arn]
}

resource "aws_iam_role" "role_anywhere" {
  name = "${var.bucket_name}-iamanywhere-trust-role"
  path = "/"
  max_session_duration = 43200

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "rolesanywhere.amazonaws.com",
        },
        Action = [
          "sts:AssumeRole",
          "sts:TagSession",
          "sts:SetSourceIdentity"
        ],
        Condition = {
          ArnEquals = {
            "aws:SourceArn" = "arn:aws:rolesanywhere:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:trust-anchor/${aws_rolesanywhere_trust_anchor.trust_anchor.id}"
          }
        }
      }
    ]
  })
}

resource "aws_iam_policy" "iamanywhere" {
  name   = "${var.bucket_name}_minimal_policy"
  policy = data.aws_iam_policy_document.s3_report_access.json
}

resource "aws_iam_role_policy_attachment" "iamanywhere" {
  role       = aws_iam_role.role_anywhere.id
  policy_arn = aws_iam_policy.iamanywhere.arn
}
```

Data sources nedan hämtar aktuellt AWS-konto, region, certifikatsecret och den S3-bucket som ska användas. De definierar också IAM policy document som används av IAM-rollen.

Policyn tillåter workloaden att skriva, läsa, lista och hantera multipart uploads för objekt i den valda reports-bucketen.

**data.tf**

```terraform
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

data "aws_secretsmanager_secret" "certificate_secret" {
  arn = var.certifcate_secret_arn
}

data "aws_secretsmanager_secret_version" "certificate_secret" {
  secret_id = data.aws_secretsmanager_secret.certificate_secret.id
}

data "aws_s3_bucket" "reports" {
  bucket = var.bucket_name
}

data "aws_iam_policy_document" "s3_report_access" {
  statement {
    sid = "S3ReportsStorageCRUD"

    actions = [
      "s3:AbortMultipartUpload",
      "s3:DeleteObject",
      "s3:ListMultipartUploadParts",
      "s3:PutObject",
      "s3:GetObject"
    ]

    resources = [
      "${data.aws_s3_bucket.reports.arn}/*"
    ]
  }

  statement {
    sid = "S3ReportsStorageList"

    actions = [
      "s3:GetBucketLocation",
      "s3:ListBucket",
      "s3:ListBucketMultipartUploads"
    ]

    resources = [
      data.aws_s3_bucket.reports.arn,
    ]
  }
}
```

Slutligen behöver modulen två input variables:

- `bucket_name`: den S3-bucket som används för rapporterna.
- `certifcate_secret_arn`: ARN:et för Secrets Manager-secretet som innehåller certificate bundle.

**variables.tf**

```terraform
variable "bucket_name" {
  type = string
}

variable "certifcate_secret_arn" {
  type = string
}
```

## Lokal autentisering med aws_signing_helper

Det sista steget är lokal AWS CLI-autentisering.

För att certifikatbaserad autentisering ska fungera från en lokal maskin eller workloadmiljö behöver du installera och konfigurera [aws-signing-helper](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/credential-helper.html).

Helpern använder:

- IAM Roles Anywhere trust anchor ARN
- IAM Roles Anywhere profile ARN
- IAM role ARN
- det utfärdade klientcertifikatet
- den matchande privata nyckeln

När AWS CLI använder den här profilen körs credential process, certifikatflödet valideras via IAM Roles Anywhere och temporära AWS-credentials returneras.

**~/.aws/config**

```bash
[profile iam_anywhere_test_reports]
region=eu-west-1
credential_process = /usr/local/bin/aws_signing_helper credential-process --trust-anchor-arn arn:aws:rolesanywhere:eu-west-1:${ACCOUNT_ID}:trust-anchor/3f0e4cb2-5715-47d9-b093-1c5d8e0ae974 --profile-arn arn:aws:rolesanywhere:eu-west-1:${ACCOUNT_ID}:profile/b548ea86-0137-4a04-bfac-f2d98ad88be0 --role-arn arn:aws:iam::${ACCOUNT_ID}:role/test-reports-trust-role --certificate ./clients/test-reports.pem --private-key ./clients/test-reports.key
```

## Resultatet

Med den här konfigurationen på plats är serviceanvändaren inte längre beroende av långlivade AWS access keys. I stället baseras åtkomsten på certifikat, IAM Roles Anywhere och kortlivade AWS-credentials.

Det ger en renare och säkrare lösning för workloads som behöver AWS-åtkomst utanför AWS, samtidigt som behörigheterna fortsatt styrs via IAM-roller och policies.

---

Det här är Markdown-versionen av ett inlägg i [Paperphytes anteckningsbok](https://paperphyte.com/anteckningsbok). Originalet finns på <https://paperphyte.com/anteckningsbok/ersatta-aws-access-keys-med-iam-roles-anywhere>. © Paperphyte AB. Ange källan med länk när du citerar eller sammanfattar texten.

Frågor om innehållet? [Kontakta Paperphyte](https://paperphyte.com/kontakt).
