안녕하세요, DevOps 팀 Sammie입니다. Hyperconnect에서는 microservice 배포와 관리를 쉽게 할 수 있도록 Kubernetes를 사용하고 있습니다. 처음에는 AWS IAM Authenticator[1]를 사용하여 인증 시스템을 구축하였으나, Google Groups로 사용자 그룹을 관리하고 싶어 다른 방법을 사용하고 있습니다. 이 글에서는 현재 사용하고 있는 Hyperconnect만의 Kubernetes 인증 시스템을 소개하려고 합니다.

TL; DR

architecture

전체 구성도는 위와 같습니다. 이 글에서 직접적으로 다루지 않는 구성 요소들 (multi-master nodes, kube-proxy, AWS load balancers, istio system 등)은 그림에 넣지 않았습니다.

  1. 먼저 사용자는 token 발급을 요청합니다. Pomerium[2]을 사용하여 Google 계정 인증을 수행하고, 사용자 group 정보를 얻습니다.
  2. Pomerium은 사용자 정보를 HTTP header에 넣어 kubehook[3]에 token 발급 요청을 포워딩합니다. mTLS를 사용하여 요청이 전달되며, 결과를 사용자에게 반환합니다.
  3. 사용자는 앞에서 발급받은 token을 사용해서 kube-apiserver에 요청을 보냅니다.
  4. kube-apiserver는 kubehook에 mTLS를 사용하여 token 검증을 요청하고, 결과에 따라 사용자의 요청을 수행합니다. kubehook은 master node에 DaemonSet으로 provisioning 되어 있으므로 node 외부로 트래픽이 노출되지 않습니다.
  5. 특정 node나 pod이 공격자에게 장악당한 상황을 가정합니다.
  6. 하지만 이 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을 사용하여 권한 관리도 할 수 있었습니다.

문제점

  1. 모든 벡엔드 개발자가 AWS 사용자 계정을 가지고 있지 않았으므로, 사용자를 생성하고 credential을 안전하게 전달해야 합니다.
  2. 존재하는 AWS 사용자 계정도 terraform 등 자동화된 도구에 의해 관리되지 않고 있었습니다. 물론 다른 인프라 구성 요소를 관리하는 데 terraform을 사용하고 있었기 때문에 도입 비용은 없었지만, AWS IAM Group은 DevOps 팀에서 직접 관리해야 한다는 문제가 있습니다.
  3. Kubernetes 구축 작업 중 Active Directory가 구축되었고, AWS SSO를 사용하여 AWS 계정에 로그인 할 수 있게 되었습니다. AWS SSO를 사용하여 짧은 시간 (1시간~12시간) 동안만 유효한 access key를 얻을 수 있었으나, cluster 구축 작업 당시에는 CLI를 사용하여 access key를 얻을 수 없었습니다. (현재는 aws-cli version 2로 가능합니다) 또한 AWS SSO를 사용해도 group 정보는 얻을 수 없었습니다.
  4. 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에 띄우기 위해 nodeSelectortolerations 설정이 필요하며, 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로 접근할 것이므로 subjectAltNameIP.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에 적용할 때는 다음 순서로 작업했습니다.

  1. kops update cluster --yes를 통해 master ASG의 설정을 적용합니다.
  2. 1개의 master ASG size를 0으로 설정하여 종료시키고, 다시 1로 설정하여 변경된 내용이 적용되도록 합니다.
  3. 사용자의 로그인 요청을 받지 않도록 load balancer에서 분리합니다.
  4. Master node에 접속하여 https://127.0.0.1:443으로 API 요청을 날려봅니다.
  5. 문제가 있다면 /var/log/kube-apiserver.log나 kubehook container log를 보고 디버깅합니다.
  6. 정상적으로 작동한다면 다시 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]이 있는데, 이를 사용해서 kubectlhp-cli를 호출하도록 할 수 있습니다. 아래는 캐시 로직이나 디버그 로직이 포함되어 있지 않은 핵심 과정입니다.

  1. 사용자가 kubectl 명령을 호출합니다.
  2. kubectl 명령이 hp-cli를 호출합니다.
  3. hp-clihttps://token.my-cluster.hyperconnect.com/ajax/ 웹 페이지를 띄웁니다.
  4. 그와 동시에 hp-clihttps://localhost:10042에 작은 웹 서버를 띄우고, 요청을 받을 때까지 기다립니다.
  5. 사용자가 Pomerium을 통해 인증을 완료하면 kubehook 페이지에서 https://localhost:10042에 token 정보를 ajax로 전송합니다.
  6. hp-cli는 token을 받아 Credentials Plugin 형식에 맞춰 stdout으로 출력합니다.
  7. 마침내 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에 인증서를 등록해야 하는 번거로움을 없앴습니다.

마지막으로 kubectlhp-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 인증 서버 설정이 끝났습니다. 아쉽게도, 이 방법에는 몇 가지 근본적인 한계가 있습니다.

  1. 제일 중요한 제약 조건으로, 이 방법을 사용하기 위해서는 kube-apiserver에 argument를 추가 할 수 있어야 합니다. 일반적으로 EKS, GKE 등 cloud managed cluster에서는 이런 기능을 제공하지 않아 사용할 수 없습니다.
  2. kubehook과 Pomerium 등 설치해야 할 구성요소가 많아 리소스가 필요하고 관리 cost가 있습니다. kubehook은 master node마다 1개씩 필요하며, Pomerium은 이중화가 필요하니 최소 6개 (authenticate, authorize, proxy * 2)가 필요하며, 이를 관리해야 합니다.
  3. CLI 지원을 위해서 프로그램을 직접 개발해야 합니다.
  4. 설정 방법이 상당히 복잡(?)합니다. 물론 충분한 시간이 있다면 가능합니다.

다만, 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

[2] https://www.pomerium.io/

[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

[15] https://github.com/FiloSottile/mkcert