안녕하세요, DevOps 팀 Sammie입니다. Hyperconnect에서는 microservice 배포와 관리를 쉽게 할 수 있도록 Kubernetes를 사용하고 있습니다. 처음에는 AWS IAM Authenticator
[1]를 사용하여 인증 시스템을 구축하였으나, Google Groups로 사용자 그룹을 관리하고 싶어 다른 방법을 사용하고 있습니다. 이 글에서는 현재 사용하고 있는 Hyperconnect만의 Kubernetes 인증 시스템을 소개하려고 합니다.
TL; DR
전체 구성도는 위와 같습니다. 이 글에서 직접적으로 다루지 않는 구성 요소들 (multi-master nodes, kube-proxy, AWS load balancers, istio system 등)은 그림에 넣지 않았습니다.
- 먼저 사용자는 token 발급을 요청합니다. Pomerium[2]을 사용하여 Google 계정 인증을 수행하고, 사용자 group 정보를 얻습니다.
- Pomerium은 사용자 정보를 HTTP header에 넣어 kubehook[3]에 token 발급 요청을 포워딩합니다. mTLS를 사용하여 요청이 전달되며, 결과를 사용자에게 반환합니다.
- 사용자는 앞에서 발급받은 token을 사용해서 kube-apiserver에 요청을 보냅니다.
- kube-apiserver는 kubehook에 mTLS를 사용하여 token 검증을 요청하고, 결과에 따라 사용자의 요청을 수행합니다. kubehook은 master node에 DaemonSet으로 provisioning 되어 있으므로 node 외부로 트래픽이 노출되지 않습니다.
- 특정 node나 pod이 공격자에게 장악당한 상황을 가정합니다.
- 하지만 이 pod은 kubehook에 연결할 수 있는 client certificate이 없으므로, kubehook에 연결 할 수 없습니다.
Kubernetes 인증 시스템에 익숙하시거나 AWS IAM Authenticator
의 작동 원리를 아시는 분은 쉽게 이해하실 것이라 생각합니다. Kubernetes 인증을 처음 접하시는 분들과 더 자세한 구성을 알고 싶으신 분들을 위해 몇 개의 단락에 걸쳐 AWS IAM Authenticator
의 원리, kubehook, Pomerium 소개 및 kube-apiserver
설정 방법을 자세히 설명드리겠습니다.
AWS IAM Authenticator의 인증방식
AWS STS (Security Token Service)
서비스에서는 GetCallerIdentity 라는 API[4]를 제공합니다. 이 API는 문자 의미 그대로 인증된 AWS IAM User (AssumeRole을 한 경우 Role)의 ARN을 반환합니다. 또한, AWS의 API 요청은 presign[5] 하여 header 없이 query string으로 인증 정보를 전달 할 수 있습니다. 이 둘을 조합해서, GetCallerIdentity
API를 호출하는 요청을 사용자 A의 credential로 presign 한 URL을 만들면, 누구나 이 URL에 GET 요청을 해서 사용자 A의 ARN을 얻을 수 있습니다.
AWS에서는 EKS의 공식 인증 방법으로 AWS IAM Authenticator
(너무 길어 aws-iam-auth
라 하겠습니다) 를 제공하고 있습니다. 코드를 뜯어보시거나, 토큰을 유심히 관찰해보신 분들은 바로 알아차리셨겠지만, aws-iam-auth
의 token은 prefix 'k8s-aws-v1.'
과 base64 encode 된 URL입니다. 그리고 이 URL은 앞에서 설명한 방식으로 만들어진 presigned URL입니다. 아래는 실제로 aws-iam-auth
에서 token을 생성하는 코드[6]입니다. (주석은 삭제했습니다.)
func (g generator) GetWithSTS(clusterID string, stsAPI *sts.STS) (Token, error) {
request, _ := stsAPI.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{})
request.HTTPRequest.Header.Add(clusterIDHeader, clusterID)
presignedURLString, err := request.Presign(requestPresignParam)
if err != nil {
return Token{}, err
}
tokenExpiration := time.Now().Local().Add(presignedURLExpiration - 1*time.Minute)
return Token{v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration}, nil
}
또한, Kubernetes에서는 인증 시스템을 customize 할 수 있도록 구축할 수 있도록 Webhook Token Authentication
을 지원[7]하고 있습니다. .kube/config
와 유사한 형식으로 webhook 인증 서버 정보를 파일에 저장하고, 이를 kube-apiserver
에 --authentication-token-webhook-config-file
인자로 넘겨주면 사용 할 수 있습니다. kops[8]에서는 cluster 설정에 authentication: aws: {}
을 추가[9]하면 aws-iam-auth
를 사용하게 되는데, 이는 kops가 앞에서 설명했던 과정을 전부 자동으로 수행해주기 때문입니다.
Hyperconnect에서는 kops로 Kubernetes cluster를 구축했으므로, 아주 쉽게 aws-iam-auth
를 사용 할 수 있었습니다. aws-iam-auth
에서 token을 생성할 때 --role
argument를 넘겨주면 AssumeRole
을 먼저 수행하고 token을 발급하기 때문에 AWS IAM Group을 사용하여 권한 관리도 할 수 있었습니다.
문제점
- 모든 벡엔드 개발자가 AWS 사용자 계정을 가지고 있지 않았으므로, 사용자를 생성하고 credential을 안전하게 전달해야 합니다.
- 존재하는 AWS 사용자 계정도 terraform 등 자동화된 도구에 의해 관리되지 않고 있었습니다. 물론 다른 인프라 구성 요소를 관리하는 데 terraform을 사용하고 있었기 때문에 도입 비용은 없었지만, AWS IAM Group은 DevOps 팀에서 직접 관리해야 한다는 문제가 있습니다.
- Kubernetes 구축 작업 중 Active Directory가 구축되었고, AWS SSO를 사용하여 AWS 계정에 로그인 할 수 있게 되었습니다. AWS SSO를 사용하여 짧은 시간 (1시간~12시간) 동안만 유효한 access key를 얻을 수 있었으나, cluster 구축 작업 당시에는 CLI를 사용하여 access key를 얻을 수 없었습니다. (현재는 aws-cli version 2로 가능합니다) 또한 AWS SSO를 사용해도 group 정보는 얻을 수 없었습니다.
- OIDC를 사용하여
kube-apiserver
에 로그인하도록 설정[10] 할 수 있습니다. 회사의 모든 직원은 회사 Google 계정을 가지고 있었으므로, 로그인 설정은 쉽게 할 수 있습니다. 그러나, Google OIDC 연동은 Google Groups의 정보를 가져올 수 없어, 2번과 마찬가지로 그룹 정보를 DevOps 팀에서 직접 관리해야 한다는 문제가 있었습니다.
Custom AuthN + AuthZ
kubehook
더 좋은 방법이 없나 고민하던 중 kubehook을 찾았습니다. kubehook은 proxy로만 접근 할 수 있다는 것을 가정하고, 특정한 HTTP header의 정보로 JWT token을 생성 및 검증해주는 서버입니다.
aws-iam-auth
와 동일하게 master node에 DaemonSet
으로 띄울 수 있습니다. Master node에 띄우기 위해 nodeSelector
와 tolerations
설정이 필요하며, hostPort
를 설정했고, 혹시 모를 CNI 장애를 대비하기 위해 hostNetwork: true
를 설정하여 CNI 없이 host 네트워크를 사용하도록 했습니다. 이렇게 하면 kube-apiserver
는 token 검증 시 localhost로만 데이터를 전송하면 되기 때문에 빠르고 안전합니다.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kubehook
labels:
app: kubehook
namespace: kube-system
spec:
selector:
matchLabels:
app: kubehook
template:
metadata:
labels:
app: kubehook
spec:
containers:
- image: <kubehook image>
name: kubehook
command:
- "/kubehook"
- "--audience=hpcnt.com/my-cluster"
- "--user-header=x-custom-user-header"
- "--group-header=x-custom-user-groups"
- "--group-header-delimiter=,"
- "--kubecfg-template=/tmp/kubehook/config/kubeconfig"
- "--tls-cert=/tmp/kubehook/certs/tls.crt"
- "--tls-key=/tmp/kubehook/certs/tls.key"
- "--client-ca=/tmp/kubehook/certs/ca.crt"
- "--client-ca-subject=my-cluster@internal.hyperconnect.com"
env:
- name: KUBEHOOK_SECRET
valueFrom:
secretKeyRef:
name: kubehook-secret
key: data
ports:
- containerPort: 10042
hostPort: 10042
volumeMounts:
- name: kubehook-cfg
mountPath: /tmp/kubehook/config
- name: kubehook-certs
mountPath: /tmp/kubehook/certs
volumes:
- name: kubehook-cfg
configMap:
name: kubehook-cfg
- name: kubehook-certs
secret:
secretName: kubehook-certs
hostNetwork: true
nodeSelector:
node-role.kubernetes.io/master: ""
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
또한, kubehook UI에서 kubeconfig 파일을 바로 다운로드받을 수 있도록 kubehook-cfg를 설정했습니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: kubehook-cfg
namespace: kube-system
data:
kubeconfig: |-
apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://my-cluster.hyperconnect.com
name: my-cluster.hyperconnect.com
다음으로 kubehook-certs에는 TLS 활성화와 mTLS까지 지원하기 위해 인증서 파일을 넣었습니다. 인증서 생성 방법은 많은 블로그[11]에 잘 설명되어 있으니, 이를 참고하여 설정하면 됩니다. 다만, Kubernetes master server에서 127.0.0.1로 접근할 것이므로 subjectAltName
에 IP.1 = 127.0.0.1
항목을 반드시 넣어야 합니다.
apiVersion: v1
kind: Secret
metadata:
name: kubehook-certs
namespace: kube-system
data:
tls.crt: |- # kubehook server tls cert
<redacted>
tls.key: |- # kubehook server tls key
<redacted>
ca.crt: |- # kubehook client ca
<redacted>
마지막으로 kops edit cluster
명령으로 kops cluster configuration에 적절한 설정을 추가하고, master node를 rolling update하면 kubehook을 사용 할 수 있게 됩니다.
spec:
fileAssets:
- content: |
apiVersion: ""
clusters:
- cluster:
certificate-authority-data: <redacted> # base64-encoded kubehook server ca
server: https://127.0.0.1:10042/authenticate
name: kubehook
contexts:
- context:
cluster: kubehook
user: kube-apiserver
name: webhook
current-context: webhook
kind: ""
users:
- name: kube-apiserver
user:
client-certificate-data: <redacted> # base64-encoded client certificate
client-key-data: <redacted> # base64-encoded client key
name: kubehook-auth-config
path: /srv/kubernetes/kubehook-auth-config
roles:
- Master
kubeAPIServer:
authenticationTokenWebhookConfigFile: /srv/kubernetes/kubehook-auth-config
혹시 모를 장애에 대비하기 위해 위 설정을 master node에 적용할 때는 다음 순서로 작업했습니다.
kops update cluster --yes
를 통해 master ASG의 설정을 적용합니다.- 1개의 master ASG size를 0으로 설정하여 종료시키고, 다시 1로 설정하여 변경된 내용이 적용되도록 합니다.
- 사용자의 로그인 요청을 받지 않도록 load balancer에서 분리합니다.
- Master node에 접속하여 https://127.0.0.1:443으로 API 요청을 날려봅니다.
- 문제가 있다면
/var/log/kube-apiserver.log
나 kubehook container log를 보고 디버깅합니다. - 정상적으로 작동한다면 다시 load balancer에 등록하고 모든 master를 rolling-update합니다.
Pomerium + kubectl
이제 사용자를 인증하고, kubehook으로 사용자 이름과 그룹 정보를 전달해 줄 프록시만 설정하면 됩니다. 직접 OAuth 웹 서버를 만들어야 하나 고민하던 중, Pomerium을 찾았습니다.
Pomerium은 Google, Azure AD, Okta 등 여러 가지 IdP를 지원하는 SSO gateway 서버입니다.
Google IdP 연동
Google IdP를 사용할 경우 Google Admin Directory API[12]를 사용하여 그룹 정보도 가져올 수 있습니다. Pomerium은 인증된 사용자의 정보를 x-pomerium-authenticated-user-*
로 전달[13]하기 때문에 kubehook에서 즉시 사용할 수 있습니다. 앞에서 mTLS를 적용했으므로 Pomerium proxy 설정에도 인증서를 추가해야 합니다. 인증서 정보와 포맷은 kube-apiserver
설정과 동일합니다.
policy:
- from: https://token.my-cluster.hyperconnect.com
to: https://kubehook.kube-system.svc.cluster.local:443
allowed_groups:
- devops@hyperconnect.com
- smart-developers@hyperconnect.com
- api-developers@hyperconnect.com
tls_server_name: 127.0.0.1
tls_custom_ca: <redacted> # base64-encoded server ca
tls_client_cert: <redacted> # base64-encoded client certificate
tls_client_key: <redacted> # base64-encoded client key
이제, 개발자는 AWS 계정이 없어도 https://token.my-cluster.hyperconnect.com
에 들어가서 Google 로그인 후 token을 발급받을 수 있습니다. 이렇게 발급받은 token을 .kube/config
에 넣으면 설정이 완료됩니다.
apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://my-cluster.hyperconnect.com
name: my-cluster.hyperconnect.com
contexts:
- context:
cluster: my-cluster.hyperconnect.com
user: token-user
name: my-cluster.hyperconnect.com
users:
- name: token-user
user:
token: <redacted> # token here
발급받은 token에는 group 정보가 있으므로, Kubernetes RBAC에서 kind: Group
을 사용하여 사용자 개인이 아닌 그룹 전체에 권한을 줄 수 있습니다. 예를 들어, 아래 RBAC 설정은 그룹 api-developers@hyperconnect.com
에 속한 모든 개발자에게 namespace api
에 대해 모든 권한을 부여합니다.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: api-developers-role
namespace: api
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: api-developers-role-binding
namespace: api
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: api-developers-role
subjects:
- kind: Group
name: api-developers@hyperconnect.com
Generate Token & Kubeconfig with CLI Tool
하지만, 뭔가 귀찮습니다. JWT token은 유효 기간이 있고, 이 기간이 끝나면 다시 token 발급 페이지에 접속해서 받은 token을 .kube/config
에 넣어야 합니다. 특히 DevOps의 경우 여러 cluster를 관리해야 하므로 귀찮음은 배가 됩니다. 그래서 Hyperconnect 개발자가 사용하고 있는 hp-cli
에 발급 자동화 기능까지 추가했습니다.
DevOps 팀에서는 Hyperconnect 개발자를 위해 CLI를 만들고 있습니다.
hp-cli
라고 하는 이 도구는 Python을 사용해서 개발되었고, 몇 가지 편리한 기능을 구현해 놓았습니다. 예를 들어hp ec2 <keyword>
를 입력하면 Hyperconnect에서 사용하는 AWS 계정의 ec2 목록을 검색하고, instance를 선택하면 private ip로 ssh 연결을 수행합니다.hp docker <image name> <image version>
을 입력하면, 명령을 수행한 directory의 git repository, branch 정보를 가져오고, 이 정보로 공용 Jenkins에서 Dockerfile을 빌드하여 이미지를 내부 repository로 push 합니다.
앞 단락에서 언급한 귀찮음을 해소하기 위해, hp-cli
에 token 발급 자동화 기능을 추가했습니다. kubectl
에는 Credentials Plugin[14]이 있는데, 이를 사용해서 kubectl
이 hp-cli
를 호출하도록 할 수 있습니다. 아래는 캐시 로직이나 디버그 로직이 포함되어 있지 않은 핵심 과정입니다.
- 사용자가
kubectl
명령을 호출합니다. kubectl
명령이hp-cli
를 호출합니다.hp-cli
는https://token.my-cluster.hyperconnect.com/ajax/
웹 페이지를 띄웁니다.- 그와 동시에
hp-cli
는https://localhost:10042
에 작은 웹 서버를 띄우고, 요청을 받을 때까지 기다립니다. - 사용자가 Pomerium을 통해 인증을 완료하면
kubehook
페이지에서https://localhost:10042
에 token 정보를 ajax로 전송합니다. hp-cli
는 token을 받아 Credentials Plugin 형식에 맞춰 stdout으로 출력합니다.- 마침내
kubectl
은 token 정보를 얻어kube-apiserver
에 API 요청을 전송합니다.
hp-cli
에 기능을 추가하기에 앞서, 5번 과정을 위해 kubehook
코드를 조금 수정했습니다. token을 발급하고 https://localhost:10042
로 ajax 요청을 하는 페이지를 추가했습니다.
func HandlerAjax(g auth.Generator, h handlers.AuthHeaders) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
token := getTokenBySomeLogic()
payload := fmt.Sprintf(`
<html><head><title>kubehook CLI</title></head><body><script>
const data = {'token': '%s'};
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://localhost:10042');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 200) {
window.close();
}
};
xhr.send(JSON.stringify(data));
</script></body></html>
`, token)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, payload)
}
}
다음으로 hp-cli
에 token을 가져오는 기능을 추가했습니다. 실제 코드에는 kubectl config use-context
기능, kops 환경변수 설정 기능이나 캐싱 로직이 더 추가되어 있습니다.
import json
import ssl
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
class KubehookServer(HTTPServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.token = None
class KubehookRequestHandler(BaseHTTPRequestHandler):
# CORS pre-flight 요청을 위해 필요합니다.
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', 'content-type')
self.end_headers()
def do_POST(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', 'content-type')
self.send_header('Content-type', 'text/html')
self.end_headers()
content = self.rfile.read(int(self.headers['Content-Length']))
self.server.token = json.loads(content.decode('utf-8'))['token']
raise KeyboardInterrupt
def format_token(token):
""" stdout으로 token을 출력합니다. Credentials Plugin spec로 출력해야 합니다. """
print('...')
def main():
# [3] token 발급 페이지를 웹 브라우저로 띄웁니다.
webbrowser.open_new_tab('https://token.my-cluster.hyperconnect.com/ajax')
httpd = KubehookServer(('localhost', 10042), KubehookRequestHandler)
httpd.socket = ssl.wrap_socket(
httpd.socket, server_side=True, certfile='localhost.crt', keyfile='localhost.key')
try:
# [4] https://localhost:10042로 요청을 받는 웹 서버를 띄웁니다.
httpd.serve_forever()
except KeyboardInterrupt:
pass
finally:
httpd.server_close()
print(format_token(httpd.token))
if __name__ == '__main__':
main()
여기서, localhost:10042
로 띄우는 웹 서버는 TLS를 지원해야 합니다. 일부 브라우저에서는 보안을 위해 https로 접속한 웹 사이트에서 http로 ajax 요청하는 것을 금지하고 있기 때문입니다. 로컬에서 신뢰 할 수 있는 인증서를 발급할 때는 mkcert[15]를 사용하여 개발자가 직접 openssl
명령을 입력해야 하거나 macOS Keychain에 인증서를 등록해야 하는 번거로움을 없앴습니다.
마지막으로 kubectl
이 hp-cli
를 사용하여 인증할 수 있도록 .kube/config
를 설정했습니다.
user:
- name: hp-cli-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
command: python
args:
- ~/hpcnt/hp-cli/kube-token.py
마무리
이렇게 custom 인증 서버 설정이 끝났습니다. 아쉽게도, 이 방법에는 몇 가지 근본적인 한계가 있습니다.
- 제일 중요한 제약 조건으로, 이 방법을 사용하기 위해서는
kube-apiserver
에 argument를 추가 할 수 있어야 합니다. 일반적으로 EKS, GKE 등 cloud managed cluster에서는 이런 기능을 제공하지 않아 사용할 수 없습니다. - kubehook과 Pomerium 등 설치해야 할 구성요소가 많아 리소스가 필요하고 관리 cost가 있습니다. kubehook은 master node마다 1개씩 필요하며, Pomerium은 이중화가 필요하니 최소 6개 (authenticate, authorize, proxy * 2)가 필요하며, 이를 관리해야 합니다.
- CLI 지원을 위해서 프로그램을 직접 개발해야 합니다.
- 설정 방법이 상당히 복잡(?)합니다. 물론 충분한 시간이 있다면 가능합니다.
다만, Hyperconnect에서는 kops를 사용해서 kube-apiserver
설정이 비교적 자유로웠고, Pomerium은 kubehook 외의 많은 internal service (Kubernetes dashboard, Kiali, Prometheus 등)을 proxy 하는 데 사용하고 있으므로 큰 문제가 없었습니다. 또한, 내부에서 사용하는 CLI 도구인 hp-cli
도 이미 개발되어 있었으므로 Kubernetes 기능을 추가하기에는 비교적 쉬웠습니다. 설정 방법이 상당히 복잡하지만, 한 번 구성한 후에는 DevOps에서 사용자 그룹을 직접 관리할 필요 없이 그룹 기반으로 Kubernetes RBAC를 설정할 수 있게 되었습니다.
처음 문제를 마주했을 때는 aws-iam-auth
가 어떻게 인증을 수행하는지 몰랐고, 이렇게 custom한 인증 시스템을 구축할 수 있을 것이라고는 전혀 기대하지 않고 Terraform 파일을 만들고 있었습니다. 이 시스템 구축을 통해서 aws-iam-auth
의 작동 원리를 정확하게 알게 되었고, Kubernetes의 막강한 확장성을 체험해 볼 수 있었습니다. 비슷한 고민을 하셨던 분들께 도움이 되었으면 좋겠습니다.
읽어주셔서 감사합니다 :)
덤 1: 사실 GKE를 사용하면 아무것도 할 필요 없이 해결됩니다. 다만 Azar RDS는 절대 옮길 수 없었으므로, AWS를 사용하게 되며 이 모든 문제가 시작되었습니다.
덤 2: 현재 DevOps 팀에서는 Kubernetes를 적극적으로 도입하면서 여러 작업을 하고 있습니다. 기회가 된다면, 배포 파이프라인이나 로깅 방법도 소개해드리겠습니다.
덤 3: Hyperconnect에서는 채용 중입니다! 저희와 같이 Hyperconnect에서 서비스와 인프라를 만들어나가고 싶은 분들의 많은 지원 부탁드립니다. 채용공고 바로가기
References
[1] https://github.com/kubernetes-sigs/aws-iam-authenticator
[3] https://github.com/planetlabs/kubehook
[4] https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
[5] https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html
[6] https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/v0.4.0/pkg/token/token.go
[7] https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
[8] https://github.com/kubernetes/kops
[9] https://github.com/kubernetes/kops/blob/release-1.14/docs/authentication.md#aws-iam-authenticator
[10] https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens
[11] https://blog.codeship.com/how-to-set-up-mutual-tls-authentication/
[12] https://developers.google.com/admin-sdk/directory/v1/reference/groups
[13] https://www.pomerium.io/docs/reference/getting-users-identity.html#headers
[14] https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins