AWS, EKS, OIDC: accedere alla risorse di AWS da Kubernetes

di Andrea Zani, in terraform,

L'idea di questo post è nata dopo una critica sollevata un amico dopo il mio insistente proliferare di post dedicati a Kubernetes in AWS. Provo a riassumere la questione scaturita dalla critica: è possibile accedere alle numerose risorse di AWS dall'interno di Kubernetes? Se sì, come? Nel dettaglio: come posso leggere un Secret, per esempio, oppure leggere un Bucket in S3 dall'interno di un Pod?

Identity federation

Chi utilizza abitualmente AWS sa che per accedere, per esempio, ad un Bucket in S3 da una Lambda si devono assegnare i permessi con le IAM Role e Policy. La cosa si risolve giustappunto con l'utilizzo di una Role alla quale va assegnata una delle Policy esistenti, oppure creando ex novo una nuova Role con una nuova Policy ad hoc da assegnare alla Lambda.

Per esempio, senza fare alcuna fatica si può usare una di queste generiche:

Oppure, partendo da una di queste, creando una nuova più restrittiva e sicura:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
            ]
        }
    ]
}

L'utilizzo delle Policy direttamente dai Pod che girano all'interno del cluster EKS non è possibile perché questi non posseggono e non possono utilizzare direttamente nessuna autorizzazione per l'accesso alle risorse di AWS. Da parte sua, Kubernetes, mette a disposizione internamente le Role RBAC per l'utilizzo e l'accesso alle sue risorse interne collegabili ai Service Account. Proprio di questo voglio scrivere in questo post: come unire questi due mondi.

AWS mette a disposizione dalla versione 1.14 di Kubernetes l'IAM Roles for Service Account (IRSA). La soluzione più semplice per implementare questa funzionalità e grazie all'OpenID che mette a disposizione EKS. In questo modo è possibile associare a uno, o più Service Account di Kubernetes, le Role IAM di AWS utilizzando l'AWS STS (AssumeRoleWithWebIdentity).

La documentazione di AWS spiega tutti i passaggi per questa operazione che in questo post tratterò con Terraform... E sono al settimo post dedicato!

Terraform

Come in tutti i post precedenti dove ho creato una cluster Kubernetes EKS con Terraform, anche questa volta non farò eccezione. Userò sempre il codice che ho mostrato e incluso più volte nelle demo con piccole modifiche, ma aggiungerò inoltre la creazione dell'Identity Federation con OpenID.

Primo passaggio è prelevare le informazioni dal cluster appena avviato:

provider "kubernetes" {
  host                   = aws_eks_cluster.demo.endpoint
  token                  = data.aws_eks_cluster_auth.demo.token
  cluster_ca_certificate = base64decode(aws_eks_cluster.demo.certificate_authority.0.data)
}


data "tls_certificate" "eks" {
  url = aws_eks_cluster.demo.identity[0].oidc[0].issuer
}

Ora posso creare l'OpenID Connect Provider:

resource "aws_iam_openid_connect_provider" "eks" {
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
  url             = aws_eks_cluster.demo.identity[0].oidc[0].issuer
}

Posso aggiungere l'IAM Policy per collegare il mondo di AWS e ai miei Service Account che utilizzerò all'interno di Kubernetes:

data "aws_iam_policy_document" "oidc_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect  = "Allow"

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub"
      values   = [
        "system:serviceaccount:default:aws-sa-s3",
        "system:serviceaccount:default:aws-sa-vpc"
        ]
    }

    principals {
      identifiers = [aws_iam_openid_connect_provider.eks.arn]
      type        = "Federated"
    }
  }
}

Notare l'action AssumeRoleWithWebIdentity e i due Service Account: aws-sa-s3 e aws-sa-vpc ai quali darò i permessi di lettura a S3 per il primo e alle VPC per il secondo. Ora posso costruire le due Role in AWS con le rispettive Custom Policy:

resource "aws_iam_role" "oidc_s3" {
  assume_role_policy = data.aws_iam_policy_document.oidc_assume_role_policy.json
  name               = "oidc_s3"
}

resource "aws_iam_policy" "policy_s3" {
  name = "policy_s3"

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
              "s3:ListAllMyBuckets",
              "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::*"
        }
    ]
    })
}

resource "aws_iam_role_policy_attachment" "role_attach_s3" {
  role       = aws_iam_role.oidc_s3.name
  policy_arn = aws_iam_policy.policy_s3.arn
}


resource "aws_iam_role" "oidc_vpc" {
  assume_role_policy = data.aws_iam_policy_document.oidc_assume_role_policy.json
  name               = "oidc_vpc"
}

resource "aws_iam_policy" "policy_vpc" {
  name = "policy_vpc"

  policy =  jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeVpcs",
            "Resource": "*"
        }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "role_attach_vpc" {
  role       = aws_iam_role.oidc_vpc.name
  policy_arn = aws_iam_policy.policy_vpc.arn
}

Non rimane che collegare i due Service Account alle due Role appena create:

resource "kubernetes_service_account" "aws-sa-s3" {
  metadata {
    name = "aws-sa-s3"
    namespace = "default"
    annotations = {
        "eks.amazonaws.com/role-arn" = aws_iam_role.oidc_s3.arn
    }
  }
}

resource "kubernetes_service_account" "aws-sa-vpc" {
  metadata {
    name = "aws-sa-vpc"
    namespace = "default"
    annotations = {
        "eks.amazonaws.com/role-arn" = aws_iam_role.oidc_vpc.arn
    }
  }
}

In questo caso l'ARN delle Role è stato aggiunto in annotations (eks.amazonaws.com/role-arn) in modo che EKS colleghi correttamente i Service Account alle Role.

Service Account

Per controllare che tutto funzioni avvio due Pod con i due Service Account appena creati:

resource "kubernetes_pod" "tests3" {
  metadata {
    name = "tests3"
    namespace = "default"
  }

  spec {
    service_account_name = kubernetes_service_account.aws-sa-s3.metadata[0].name
    container {
      image = "amazon/aws-cli:2.7.12"
      name  = "aws-cli"
      command = [ "/bin/bash", "-c", "--" ]
      args = [ "sleep infinity" ]
   }
  }
}

resource "kubernetes_pod" "testvpc" {
  metadata {
    name = "testvpc"
    namespace = "default"
  }

  spec {
    service_account_name = kubernetes_service_account.aws-sa-vpc.metadata[0].name
    container {
      image = "amazon/aws-cli:2.7.12"
      name  = "aws-cli"
      command = [ "/bin/bash", "-c", "--" ]
      args = [ "sleep infinity" ]
   }
  }
}

Le due righe importanti in queste due file per Terraform sono quelle in cui viene definito il Service Account da utilizzare per quel Pod: aws-sa-s3 per il primo Pod, e aws-sa-vpc per il secondo Pod. Ma per verificare che tutto funzioni è arrivato il momento di avviare il tutto in AWS in modo che venga creato un cluster EKS. La procedura, ripetuta in questi post per troppe volte, è la seguente:

terraform init
terraform plan
terraform apply -auto-approve

Dopo una decina di minuti di attesa, se tutto ha funzionato, saranno presenti i due Pod. Posso verificare, ma prima devo aggiornare il file config nella directory .kube dell'utente utilizzato sulla macchina da cui si è lanciato il tutto. Uso il comando:

aws eks --region eu-south-1 update-kubeconfig --name K8sDemo-blog-service-account
 

Quindi verifico che i due Pod sono attivi:

$ kubectl get po
NAME      READY   STATUS    RESTARTS   AGE
tests3    1/1     Running   0          115s
testvpc   1/1     Running   0          115s

Al primo Service Account era stato assegnato la Role per poter accedere a S3. Controllo che funzioni:

$ kubectl exec tests3 -- aws s3api list-buckets
{
    "Buckets": [
        {
            "Name": "MyBucket",
            "CreationDate": "2022-07-03T11:49:33+00:00"
        }
    ],
    "Owner": {
        "ID": "72e08b113af8bb54a4876a672d637a12451faf9134d50ae1933b48291ba4f31d"
    }
}

Ottimo, ecco il mio unico Bucket in S3.

Il secondo Pod può richiedere la lista delle VPC nella zona dove è stato avviato il Cluster di Kubernetes:

$ kubectl exec testvpc -- aws ec2 describe-vpcs
{
    "Vpcs": [
        {
            "CidrBlock": "10.0.0.0/16",
            "DhcpOptionsId": "dopt-8f6780e6",
            "State": "available",
            "VpcId": "vpc-0dcd6cbf4e03d51dc",
            "OwnerId": "838080890745",
            "InstanceTenancy": "default",
            "CidrBlockAssociationSet": [
                {
                    "AssociationId": "vpc-cidr-assoc-0b29d742ced10ee16",
                    "CidrBlock": "10.0.0.0/16",
                    "CidrBlockState": {
                        "State": "associated"
                    }
                }
            ],
            "IsDefault": false,
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "AZ-vpc-blog-service-account"
                },
                {
                    "Key": "kubernetes.io/cluster/K8sDemo-blog-service-account",
                    "Value": "shared"
                }
            ]
        },
        {
            "CidrBlock": "172.31.0.0/16",
            "DhcpOptionsId": "dopt-8f6780e6",
            "State": "available",
            "VpcId": "vpc-876582ee",
            "OwnerId": "838080890745",
            "InstanceTenancy": "default",
            "CidrBlockAssociationSet": [
                {
                    "AssociationId": "vpc-cidr-assoc-421afd2b",
                    "CidrBlock": "172.31.0.0/16",
                    "CidrBlockState": {
                        "State": "associated"
                    }
                }
            ],
            "IsDefault": true
        }
    ]
}

Le Role con i Service Account funzionano correttamente. Ma se invertissi i comandi nei due Pod?

$ kubectl exec testvpc -- aws s3api list-buckets

An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied
command terminated with exit code 254

$ kubectl exec tests3 -- aws ec2 describe-vpcs

An error occurred (UnauthorizedOperation) when calling the DescribeVpcs operation: You are not authorized to perform this operation.
command terminated with exit code 254

Mi ritengo moderatamente soddisfatto.

Divagazione tecnica

L'aggiunta di questo Service Account nel Pod provoca la creazione di una nuova directory e il Mount della stessa (come è visibile utilizzando il comando describe sul Pod). Entrando nel Pod con una Bash (non spiego come si fa, perché se si tratta questo argomento si deve conoscere almeno le basi di Kubernetes), controllo il contenuto di questa directory:

cd /var/run/secrets/eks.amazonaws.com/serviceaccount

Qui è presente un file con il nome token di cui controllo il contenuto:

$ cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6ImE4YWQzMDAyZGFlZjlmYjEzZWNkZmRlYzljYjI0OGVkMzIwM2EyYmYifQ.eyJhdWQiOlsic3RzLmFtYXpvbmF3cy5jb20iXSwiZXhwIjoxNjU3OTA3NTg0LCJpYXQiOjE2NTc4MjExODQsImlzcyI6Imh0dHBzOi8vb2lkYy5la3MuZXUtc291dGgtMS5hbWF6b25hd3MuY29tL2lkL0Q1RDAzMkIwQzkyRTg3MzY3QThEREFFRDYxN0U3NUMwIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJ0ZXN0czMiLCJ1aWQiOiJmYTlmYmZmYi1kMGE0LTQxOWItYmU3OC03YWNkZDUwODA0NjMifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImF3cy1zYS1zMyIsInVpZCI6IjVjZWM1MmQyLTMyNjItNDE5Mi1iZmEzLWM5N2FiYTI1ZGVmYSJ9fSwibmJmIjoxNjU3ODIxMTg0LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDphd3Mtc2EtczMifQ.i-lBS-t_T4UGBE7kQjIQ536QwoToN9_j_YG0eNwKfIbzuFfs7bLUIyPTYYYObIbgkBLgBpXLnTwuDvG7x-hVufZjAJiXI1sZFfbGUnZ1cUso9Loq9hWdHleiBwRe_I385dTxJbwMUrUNadIfonLlHFsPZEb5QKfO3nYoy4vHIqW9pSPVyqvihjteQ_RxPmCfjkvMGVs-5EJH8hbDH6IyM5EmbpQAWij_W6OxViYHMMqRvoAlQpfZUagdJHbEFR5sfWeGJ2eUfc_0YmoLZRQxwMUtu9ziq8PPKR5_cwqddCuhBOgsRmhZ3DEaSBK8UbqaK8UkxrqJn-BcCCRFida_ug

Utilizzando il sito jwt.io controllo il contenuto:

{
  "aud": [
    "sts.amazonaws.com"
  ],
  "exp": 1657907584,
  "iat": 1657821184,
  "iss": "https://oidc.eks.eu-south-1.amazonaws.com/id/D5D032B0C92E87367A8DDAED617E75C0",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "tests3",
      "uid": "fa9fbffb-d0a4-419b-be78-7acdd5080463"
    },
    "serviceaccount": {
      "name": "aws-sa-s3",
      "uid": "5cec52d2-3262-4192-bfa3-c97aba25defa"
    }
  },
  "nbf": 1657821184,
  "sub": "system:serviceaccount:default:aws-sa-s3"
}

E sarà usato questo Token per le richieste API. La durata di questo Token è di ventiquattro ore. Ma ci penserà il servizio kubelet in Kubernetes a ricrearlo prima della sua scadenza.

Conclusioni

Ora se all'interno dei miei Pod avessi bisogno di un gestore di Queue come SNS/SQS, posso farlo. Devo accedere ai Bucket in S3 per la gestione dei file, posso farlo. Voglio accedere ai Secret in AWS dove ho salvato le credenziali in modo sicuro, posso farlo. Il cluster Kubernetes in EKS ora non è più ambiente chiuso dove tutto deve vivere al suo interno. Mi piace.

Link per il codice della demo.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese