Kubernetes 인증 방식

Kubernetes 인증 방식

참고: 이 엔진은 TLS 또는 서명 검증의 일부로 외부 X.509 인증서를 사용할 수 있어요. SHA-1을 사용하는 X.509 인증서에 대한 서명 검증은 구식화(Deprecated)되었고 Vault 1.12부터 워크어라운드 없이는 더 이상 사용할 수 없어요. 자세한 내용은 구식화 공지를 참고하세요.

kubernetes 인증 방식은 Kubernetes Service Account Token을 사용해 Vault로 인증할 수 있게 해요. 이 인증 방식은 Kubernetes 파드에 Vault 토큰을 쉽게 도입하게 해줘요.

Kubernetes Service Account Token을 사용해 JWT 인증으로 로그인할 수도 있어요. JWT 인증을 대신 사용하고 싶은 이유와 Kubernetes 인증과 어떻게 비교되는지에 대한 요약은 수명이 짧은 Kubernetes 토큰 다루는 방법 섹션을 참고하세요.

참고: Kubernetes v1.21+로 업그레이드한다면 구성 옵션 disable_iss_validation이 true로 설정되어 있는지 확인하세요. 기본 마운트 경로를 가정하면 vault read -field disable_iss_validation auth/kubernetes/config로 확인할 수 있어요. 자세한 내용은 아래 Kubernetes 1.21을 참고하세요.

출처: 문서

본문

인증 (Authentication)

CLI를 통해 (Via the CLI)

기본 경로는 /kubernetes예요. 이 인증 방식이 다른 경로에 활성화되었다면 kubernetes 대신 그 값을 사용하세요.

$ vault write auth/kubernetes/login role=demo jwt=...

API를 통해 (Via the API)

기본 엔드포인트는 auth/kubernetes/login이에요. 이 인증 방식이 다른 경로에 활성화되었다면 kubernetes 대신 그 값을 사용하세요.

$ curl \
    --request POST \
    --data '{"jwt": "<your service account jwt>", "role": "demo"}' \
    http://127.0.0.1:8200/v1/auth/kubernetes/login

응답은 auth.client_token에 토큰을 포함해요:

{
  "auth": {
    "client_token": "38fe9691-e623-7238-f618-c94d4e7bc674",
    "accessor": "78e87a38-84ed-2692-538f-ca8b9f400ab3",
    "policies": ["default"],
    "metadata": {
      "role": "demo",
      "service_account_name": "myapp",
      "service_account_namespace": "default",
      "service_account_secret_name": "myapp-token-pd21c",
      "service_account_uid": "aa9aa8ff-98d0-11e7-9bb7-0800276d99bf"
    },
    "lease_duration": 2764800,
    "renewable": true
  }
}

구성 (Configuration)

인증 방식은 사용자나 머신이 인증하기 전에 미리 구성되어야 해요. 이 단계들은 보통 운영자나 설정 관리 도구가 수행해요.

1. Kubernetes 인증 방식을 활성화합니다.

$ vault auth enable kubernetes

2. /config 엔드포인트를 사용해 Vault가 Kubernetes와 통신하도록 구성합니다. kubectl cluster-info를 사용해 Kubernetes 호스트 주소와 TCP 포트를 검증하세요. 사용 가능한 구성 옵션 목록은 API 문서를 참고하세요.

$ vault write auth/kubernetes/config \
    token_reviewer_jwt="<your reviewer service account JWT>" \
    kubernetes_host=https://192.168.99.100:<your TCP port or blank for 443> \
    [email protected]

참고: Vault가 파드를 인증하는 패턴은 네트워크를 통해 JWT 토큰을 공유하는 데 의존해요. Vault의 보안 모델을 고려하면 Vault가 신뢰할 수 있는 컴퓨팅 베이스의 일부이므로 허용돼요. 일반적으로 Kubernetes 애플리케이션은 이 JWT를 다른 애플리케이션과 공유하지 않아야 해요. 파드를 대신해 API 호출을 수행할 수 있고 제3자에게 의도하지 않은 접근을 부여할 수 있기 때문이에요.

3. 이름 있는 역할을 만듭니다.

$ vault write auth/kubernetes/role/demo \
    bound_service_account_names=myapp \
    bound_service_account_namespaces=default \
    policies=default \
    audience=myapp \
    ttl=1h

이 역할은 default 네임스페이스의 "myapp" 서비스 계정을 인가하고 default 정책을 줘요.

전체 구성 옵션 목록은 API 문서를 참고하세요.

Kubernetes 1.21

1.21 버전부터 Kubernetes BoundServiceAccountTokenVolume 기능이 기본적으로 활성화돼요. 이는 기본적으로 컨테이너에 마운트되는 JWT 토큰을 Kubernetes 인증에 중요한 두 가지 방식으로 변경해요:

  • 만료 시간이 있고 파드와 서비스 계정의 수명에 바인딩돼요.
  • JWT의 "iss" 클레임 값이 클러스터의 구성에 따라 달라져요.

토큰 수명의 변경은 token_reviewer_jwt 옵션 구성에 중요해요. 수명이 짧은 토큰을 사용하면 Kubernetes는 파드나 서비스 계정이 삭제되거나 만료 시간이 지나자마자 토큰을 폐기하고, Vault는 더 이상 TokenReview API를 사용할 수 없어요. 이 변경을 처리하는 방법은 아래 수명이 짧은 Kubernetes 토큰 다루는 방법을 참고하세요.

발급자 변경에 대응해 Kubernetes 인증은 Vault 1.9.0에서 기본적으로 발급자를 검증하지 않도록 업데이트되었어요. Kubernetes API는 토큰을 검토할 때 동일한 검증을 수행하므로 Vault 측에서 발급자 검증을 활성화하는 것은 중복 작업이에요. Vault의 발급자 검증을 비활성화하지 않으면 단일 Kubernetes 인증 구성이 Kubernetes 1.20과 1.21 양쪽 모두에서 기본 마운트된 파드 토큰과 작동할 수 없어요. Vault 1.9 이전에 생성된 인증 마운트는 이전 기본값을 유지하므로 Kubernetes를 1.21로 업그레이드하기 전에 disable_iss_validation=true를 명시적으로 설정해야 해요. Vault에서 발급자 검증을 활성화하려면 아래 서비스 계정 issuer 찾기를 참고하세요.

수명이 짧은 kubernetes 토큰 다루는 방법 (How to work with short-lived kubernetes tokens)

기본으로 마운트된 파드 토큰이 수명이 짧을 때 Kubernetes 파드 인증을 구성하는 몇 가지 방법이 있으며 각각 고유한 장단점이 있어요. 다음 표는 각각 아래에 더 자세히 설명된 옵션을 요약해요.

옵션 모든 토큰 수명이 짧음 토큰 조기 폐기 가능 기타 고려 사항
로컬 토큰을 검토자 JWT로 사용 예 예 Vault(1.9.3+)를 Kubernetes 클러스터에 배포해야 함
클라이언트 JWT를 검토자 JWT로 사용 예 예 운영 오버헤드
장수명 토큰을 검토자 JWT로 사용 아니오 예
JWT 인증 대신 사용 예 아니오

참고: 기본적으로 Kubernetes는 현재 admission 주입된 서비스 계정 토큰의 수명을 1년으로 연장해 수명이 짧은 토큰으로의 전환을 완만하게 해요. 이것을 비활성화하려면 kube-apiserver에 대해 --service-account-extend-token-expiration=false를 설정하거나 자체 serviceAccountToken 볼륨 마운트를 지정하세요. 예시는 여기를 참고하세요.

로컬 서비스 계정 토큰을 검토자 JWT로 사용 (Use local service account token as the reviewer JWT)

Vault를 Kubernetes 파드에서 실행할 때 권장 옵션은 파드의 로컬 서비스 계정 토큰을 사용하는 것이에요. Vault는 수명이 짧은 토큰을 지원하기 위해 파일을 주기적으로 다시 읽어요. 로컬 토큰과 CA 인증서를 사용하려면 인증 방식 구성 시 token_reviewer_jwt와 kubernetes_ca_cert를 생략하세요. Vault는 기본 마운트 폴더 /var/run/secrets/kubernetes.io/serviceaccount/ 안의 token과 ca.crt에서 그들을 로드하려 시도해요.

$ vault write auth/kubernetes/config \
    kubernetes_host=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT

참고: Vault 1.9.3+ 필요해요. 이전 버전에서는 서비스 계정 토큰과 CA 인증서를 한 번 읽어 Vault 스토리지에 저장해요. 서비스 계정 토큰이 만료되거나 폐기되면 Vault는 더 이상 TokenReview API를 사용할 수 없고 클라이언트 인증이 실패해요.

신뢰 저장소를 CA 인증서에 사용할 수 있어요. kubernetes_ca_cert를 설정하지 않고 disable_local_ca_jwt를 true로 설정하면 Vault는 TLS 인증서 검증에 시스템의 신뢰 저장소를 사용해요.

Vault 클라이언트의 JWT를 검토자 JWT로 사용 (Use the Vault client's JWT as the reviewer JWT)

Kubernetes 인증을 구성할 때 token_reviewer_jwt를 생략할 수 있고, Vault는 Kubernetes TokenReview API와 통신할 때 Vault 클라이언트의 JWT를 자체 인증 토큰으로 사용해요. Vault가 Kubernetes에서 실행 중이라면 disable_local_ca_jwt=true도 설정해야 해요.

이는 Vault가 어떤 JWT도 저장하지 않고 모든 곳에서 수명이 짧은 토큰을 사용할 수 있게 하지만, Vault로 인증하려는 서비스 계정 집합에 클러스터 역할 바인딩을 유지하는 일부 운영 오버헤드를 추가해요. Vault의 각 클라이언트는 system:auth-delegator ClusterRole이 필요해요:

$ kubectl create clusterrolebinding vault-client-auth-delegator \
    --clusterrole=system:auth-delegator \
    --group=group1 \
    --serviceaccount=default:svcaccount1 \
    ...
장수명 토큰 계속 사용하기 (Continue using long-lived tokens)

여기의 지침을 사용해 장수명 시크릿을 만들고 그것을 token_reviewer_jwt로 사용할 수 있어요. 이 예시에서 vault 서비스 계정은 system:auth-delegator ClusterRole이 필요해요:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: vault-k8s-auth-secret
  annotations:
    kubernetes.io/service-account.name: vault
type: kubernetes.io/service-account-token
EOF

이를 사용하면 이전 워크플로를 유지하지만 수명이 짧은 토큰의 개선된 보안 자세로부터 이점을 얻지는 못해요.

JWT 인증 사용하기 (Use JWT auth)

Kubernetes 인증은 Kubernetes의 TokenReview API를 사용하도록 특화되어 있어요. 그러나 Kubernetes가 생성한 JWT 토큰은 Kubernetes를 OIDC 프로바이더로 사용해 검증할 수도 있어요. JWT 인증 방식 문서에는 Kubernetes를 OIDC 프로바이더로 JWT 인증을 설정하는 지침이 있어요.

이 솔루션은 모든 클라이언트에 수명이 짧은 토큰을 사용할 수 있게 하고 검토자 JWT의 필요를 없애줘요. 그러나 클라이언트 토큰은 TTL이 만료되기 전에 폐기될 수 없으므로 그 제한을 염두에 두고 TTL을 짧게 유지하는 것을 권장해요.

서비스 계정 issuer 찾기 (Discovering the service account issuer)

참고: Vault 1.9.0부터 disable_iss_validation과 issuer는 구식화되었고 새 Kubernetes 인증 마운트에 대한 disable_iss_validation의 기본값이 true로 변경됐어요. 다음 섹션은 disable_iss_validation=false를 설정했거나 1.9 이전에 기본값으로 마운트를 만들었을 때만 적용돼요. 하지만 disable_iss_validation=true는 모든 Vault 버전에 대한 새 권장 값이에요.

Kubernetes 1.21+ 클러스터는 서비스 계정 issuer를 kube-apiserver의 --service-account-issuer 플래그와 같은 값으로 설정해야 할 수 있어요. 이는 이러한 클러스터의 서비스 계정 JWT가 이전 기본값인 kubernetes/serviceaccount 대신 클러스터 자체에 특정한 발급자를 가질 수 있기 때문이에요. 이 값을 직접 확인할 수 없다면 다음을 실행하고 "iss" 필드를 찾아 필요한 값을 찾을 수 있어요:

$ echo '{"apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest"}' \
  | kubectl create -f- --raw /api/v1/namespaces/default/serviceaccounts/default/token \
  | jq -r '.status.token' \
  | cut -d . -f2 \
  | base64 -d

대부분의 클러스터는 .well-known/openid-configuration 엔드포인트에서도 그 정보를 사용할 수 있어요:

$ kubectl get --raw /.well-known/openid-configuration | jq -r .issuer

이 값은 Kubernetes 인증 구성 시 사용돼요. 예:

$ vault write auth/kubernetes/config \
    kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT" \
    issuer="\"test-aks-cluster-dns-d6cbb78e.hcp.uksouth.azmk8s.io\""

Kubernetes 구성하기 (Configuring kubernetes)

이 인증 방식은 제공된 JWT가 여전히 유효한지 검증하기 위해 Kubernetes TokenReview API에 접근해요. Kubernetes는 --service-account-lookup으로 실행되어야 해요. 이는 Kubernetes 1.7부터 기본적으로 true예요. 그렇지 않으면 Kubernetes에서 삭제된 토큰이 제대로 폐기되지 않고 이 인증 방식으로 인증할 수 있어요.

이 인증 방식에서 사용되는 서비스 계정은 TokenReview API에 접근할 수 있어야 해요. Kubernetes가 RBAC 역할을 사용하도록 구성되어 있다면 서비스 계정에 이 API에 접근 권한이 부여되어야 해요. 다음 예시 ClusterRoleBinding을 사용해 이 권한을 부여할 수 있어요:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: role-tokenreview-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
  - kind: ServiceAccount
    name: vault-auth
    namespace: default

API

Kubernetes Auth Plugin은 완전한 HTTP API를 제공해요. 자세한 내용은 API 문서를 참고하세요.

Terraform

Vault Terraform 프로바이더로 Kubernetes 인증 리소스를 프로그래밍 방식으로 관리할 수 있어요. 자세한 내용은 Terraform Registry 문서를 참고하세요.

워크플로 (Workflows)

Kubernetes 인증 방식 사용에 대한 다음 워크플로 예시를 참고하세요:

템플릿 정책 작업하기 (Working with templated policies)

Kubernetes 인증 구성에서 use_annotations_as_alias_metadata=true를 설정해 Vault 별칭 메타데이터에 Kubernetes Service Account 주석을 사용하세요.

use_annotations_as_alias_metadata가 true일 때 템플릿 정책을 만들 때 identity.entity.aliases.<mount accessor>.metadata.<metadata key> 템플릿 파라미터를 사용할 수 있어요.

주석을 별칭 메타데이터로 사용하려면 Vault에 Kubernetes API에서 서비스 계정을 읽을 권한을 줘야 해요.

시나리오 소개 (Scenario Introduction)

다음 정책 요구 사항이 있다고 가정해요:

애플리케이션은 할당된 키/값 시크릿 경로 (env-kv/data/<env>)에서 읽기 작업을 수행할 수 있다.

Kubernetes Service Account에 전용 시크릿 경로를 주석으로 달기
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app
  namespace: demo
  annotations:
    vault.hashicorp.com/alias-metadata-env: demo/app

애플리케이션 app이 서비스 계정의 JWT로 로그인하면 Vault는 별칭 메타데이터를 env : demo/app으로 렌더링해요.

템플릿 ACL 정책 만들기

env-tmpl 정책은 애플리케이션이 KV v2 시크릿 엔진에 정의된 시크릿을 읽을 수 있게 해요. sys/auth 엔드포인트나 vault auth list 명령에서 마운트 액세서 값(auth_kubernetes_bcecb1e1)을 사용하세요.

$ tee env-tmpl.hcl <<EOF
path "env-kv/data/{{identity.entity.aliases.auth_kubernetes_bcecb1e1.metadata.env}}" {
  capabilities = [ "read" ]
}
EOF
$ vault policy write env-tmpl env-tmpl.hcl
템플릿 ACL 정책으로 Kubernetes 역할 만들기

Kubernetes 역할은 사용자가 env-reader 역할로 로그인해 env-tmpl 정책에 설명된 시크릿 경로에서 읽을 수 있게 해요.

$ vault write auth/kubernetes/role/env-reader \
    bound_service_account_names=app \
    bound_service_account_namespaces=demo \
    policies=default,env-tmpl \
    ttl=1h

코드 예시 (Code example)

다음 예시는 Vault로 인증하는 Kubernetes 인증 방식을 보여줘요. (Go/C# 코드)

Go:

package main

import (
    "fmt"
    "os"

    vault "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/kubernetes"
)

// Fetches a key-value secret (kv-v2) after authenticating to Vault with a Kubernetes service account.
// For a more in-depth setup explanation, please see the relevant readme in the hashicorp/vault-examples repo.
func getSecretWithKubernetesAuth() (string, error) {
    // If set, the VAULT_ADDR environment variable will be the address that
    // your pod uses to communicate with Vault.
    config := vault.DefaultConfig() // modify for more granular configuration

    client, err := vault.NewClient(config)
    if err != nil {
        return "", fmt.Errorf("unable to initialize Vault client: %w", err)
    }

    // The service-account token will be read from the path where the token's
    // Kubernetes Secret is mounted. By default, Kubernetes will mount it to
    // /var/run/secrets/kubernetes.io/serviceaccount/token, but an administrator
    // may have configured it to be mounted elsewhere.
    // In that case, we'll use the option WithServiceAccountTokenPath to look
    // for the token there.
    k8sAuth, err := auth.NewKubernetesAuth(
        "dev-role-k8s",
        auth.WithServiceAccountTokenPath("path/to/service-account-token"),
    )
    if err != nil {
        return "", fmt.Errorf("unable to initialize Kubernetes auth method: %w", err)
    }

    authInfo, err := client.Auth().Login(context.TODO(), k8sAuth)
    if err != nil {
        return "", fmt.Errorf("unable to log in with Kubernetes auth: %w", err)
    }
    if authInfo == nil {
        return "", fmt.Errorf("no auth info was returned after login")
    }

    // get secret from Vault, from the default mount path for KV v2 in dev mode, "secret"
    secret, err := client.KVv2("secret").Get(context.Background(), "creds")
    if err != nil {
        return "", fmt.Errorf("unable to read secret: %w", err)
    }

    // data map can contain more than one key-value pair,
    // in this case we're just grabbing one of them
    value, ok := secret.Data["password"].(string)
    if !ok {
        return "", fmt.Errorf("value type assertion failed: %T %#v", secret.Data["password"], secret.Data["password"])
    }

    return value, nil
}

C#:

using System;
using System.IO;
using VaultSharp;
using VaultSharp.V1.AuthMethods;
using VaultSharp.V1.AuthMethods.Kubernetes;
using VaultSharp.V1.Commons;

namespace Examples
{
    public class KubernetesAuthExample
    {
        const string DefaultTokenPath = "path/to/service-account-token";

        // Fetches a key-value secret (kv-v2) after authenticating to Vault with a Kubernetes service account.
        // For a more in-depth setup explanation, please see the relevant readme in the hashicorp/vault-examples repo.
        public string GetSecretWithK8s()
        {
            var vaultAddr = Environment.GetEnvironmentVariable("VAULT_ADDR");
            if(String.IsNullOrEmpty(vaultAddr))
            {
                throw new System.ArgumentNullException("Vault Address");
            }

            var roleName = Environment.GetEnvironmentVariable("VAULT_ROLE");
            if(String.IsNullOrEmpty(roleName))
            {
                throw new System.ArgumentNullException("Vault Role Name");
            }

            // Get the path to service account token or fall back on default path
            string pathToToken = String.IsNullOrEmpty(Environment.GetEnvironmentVariable("SA_TOKEN_PATH")) ? DefaultTokenPath : Environment.GetEnvironmentVariable("SA_TOKEN_PATH");
            string jwt = File.ReadAllText(pathToToken);

            IAuthMethodInfo authMethod = new KubernetesAuthMethodInfo(roleName, jwt);
            var vaultClientSettings = new VaultClientSettings(vaultAddr, authMethod);

            IVaultClient vaultClient = new VaultClient(vaultClientSettings);

            // We can retrieve the secret after creating our VaultClient object
            Secret<SecretData> kv2Secret = null;
            kv2Secret = vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(path: "/creds").Result;

            var password = kv2Secret.Data.Data["password"];

            return password.ToString();
        }
    }
}

더 알아보기 (Learn more)