Create Identity¶
In this section you will create an identity for a namespace in your cluster. Specifically, you will
- create a Kubernetes service account,
- create a Role and RoleBinding to allow the service account to create tokens, and
- obtain the long-lived service account token
You will use this token later to Create Short-Lived Token to Authenticate External Client.
Choose one of the following options to create the identity:
Overview
What You’ll Need¶
- A Python environment with latest Arrikto wheels installed.
- Enough permissions to create the necessary resources in the desired namespace.
- The Jinja2 command-line tool.
Note
All requirements are met inside your management environment. The first two requirements are met inside a notebook in the desired namespace.
Procedure¶
Create a service account automatically or manually, by choosing one of the following options.
Option 1: Create Identity Automatically (preferred)¶
Connect to your management environment or a terminal on your notebook server.
Copy and paste the following code inside
sa-create.py
:sa-create.py1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 # 4-235 4 # This file is part of Rok. 5 # 6 # Copyright © 2020-2022 Arrikto Inc. All Rights Reserved. 7 8 """Script to create a ServiceAccount for an external client. 9 10 This script aids on getting or creating a ServiceAccount in a specific 11 namespace and returns its long-lived token. The token can be used in an 12 external client for authentication purposes. 13 """ 14 15 import os 16 import sys 17 import stat 18 import time 19 import base64 20 import logging 21 22 from rok_kubernetes import config, models 23 from rok_kubernetes.exceptions import ApiException 24 25 from rok_common.encoding import force_unicode 26 from rok_common.fileutils import dump_to_file 27 28 from rok_kubernetes.client import (RoleClient, SecretClient, RoleBindingClient, 29 ServiceAccountClient) 30 31 from rok_tasks import frontend, question 32 from rok_tasks.frontend.cli import RokCLI 33 34 log = logging.getLogger(__name__) 35 fr = frontend.get_frontend() 36 37 TITLE = "Kubernetes ServiceAccount Creator" 38 DESCRIPTION = "Generate a ServiceAccount on Kubernetes" 39 40 # Questions 41 ###################################################### 42 43 44 class ServiceAccountName(question.LineInputQuestion): 45 """Question for Service Account Name.""" 46 47 name = "question/service_account_name" 48 msg = "What is the name of the ServiceAccount?" 49 required = True 50 argument_opts = { 51 "name": ["--service-account-name"], 52 "metavar": "SERVICE_ACCOUNT_NAME", 53 "help": "Use ServiceAccount with name %(metavar)s" 54 } 55 56 57 class ServiceAccountNamespace(question.LineInputQuestion): 58 """Question for Service Account Namespace.""" 59 60 def __init__(self, *args, **kwargs): 61 super(ServiceAccountNamespace, self).__init__(*args, **kwargs) 62 sa_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 63 if os.path.exists(sa_path): 64 with open(sa_path) as f: 65 self.default = f.readline() 66 67 name = "question/service_account_namespace" 68 msg = "What is the namespace of the ServiceAccount?" 69 required = True 70 argument_opts = { 71 "name": ["--service-account-namespace"], 72 "metavar": "SERVICE_ACCOUNT_NAMESPACE", 73 "help": "Use ServiceAccount with namespace %(metavar)s" 74 } 75 76 77 # Helpers 78 ###################################################### 79 80 81 def load_kube_config(): 82 # Load K8s configuration, if present 83 if "KUBERNETES_SERVICE_HOST" in os.environ: 84 config.load_incluster_config() 85 else: 86 try: 87 config.load_kube_config() 88 except IOError as e: 89 raise RuntimeError("No K8s configuration was found: %s" % e) 90 91 92 def get_or_create_service_account(body): 93 name, namespace = body.metadata.name, body.metadata.namespace 94 sa_client = ServiceAccountClient() 95 log.info("Checking if the ServiceAccount '%s' exists in namespace '%s'..." 96 % (name, namespace)) 97 try: 98 sa = sa_client.get(name, namespace=namespace) 99 log.info("ServiceAccount already exists") 100 except ApiException as e: 101 if e.status == 404: 102 sa = sa_client.create(body, namespace=namespace) 103 return sa 104 105 106 def get_sa_token_role(sa): 107 meta = models.V1ObjectMeta(name="%s-create-token" % sa.metadata.name, 108 namespace=sa.metadata.namespace) 109 rule = models.V1PolicyRule(api_groups=[""], 110 resources=["serviceaccounts/token"], 111 resource_names=[sa.metadata.name], 112 verbs=["create"]) 113 return models.V1Role(api_version="rbac.authorization.k8s.io/v1", 114 kind="Role", metadata=meta, rules=[rule]) 115 116 117 def get_role_binding(sa, role): 118 meta = models.V1ObjectMeta(name="%s-create-token" % sa.metadata.name, 119 namespace=sa.metadata.namespace) 120 role_ref = models.V1RoleRef(api_group="rbac.authorization.k8s.io", 121 kind="Role", name=role.metadata.name) 122 subject = models.V1Subject(kind="ServiceAccount", name=sa.metadata.name, 123 namespace=sa.metadata.namespace) 124 return models.V1RoleBinding(api_version="rbac.authorization.k8s.io/v1", 125 kind="RoleBinding", metadata=meta, 126 role_ref=role_ref, subjects=[subject]) 127 128 129 def get_sa_token(sa): 130 if not sa.secrets: 131 raise ValueError("No tokens exist for the specific ServiceAccount") 132 secret_name = sa.secrets[0].name 133 secret = SecretClient().get(secret_name, namespace=sa.metadata.namespace) 134 if secret.data.get("token") is None: 135 msg = "The ServiceAccount secret is not populated with a token" 136 raise RuntimeError(msg) 137 return base64.b64decode(secret.data.get("token")) 138 139 140 def authorize_token_creation(sa): 141 role_client = RoleClient() 142 role, _ = role_client.get_or_create(get_sa_token_role(sa), 143 namespace=sa.metadata.namespace) 144 rb_client = RoleBindingClient() 145 rb_client.get_or_create(get_role_binding(sa, role), 146 namespace=sa.metadata.namespace) 147 148 149 # CLI 150 ###################################################### 151 152 153 class ServiceAccountCreator(RokCLI): 154 """Create a ServiceAccount and return its long-lived token.""" 155 156 IDENTIFIABLE_LABELS = {"arrikto.com/service-account-creator": ""} 157 DEFAULT_LOGFILE = "service_account_creator.log" 158 DEFAULT_FRONTEND = "readline" 159 160 def initialize_parser(self): 161 parser = super(ServiceAccountCreator, self).initialize_parser() 162 questions = [ServiceAccountName(), ServiceAccountNamespace()] 163 self.questions = questions 164 question.add_questions_args(questions, parser) 165 return parser 166 167 def _ask_questions(self): 168 for q in self.questions: 169 self.qctx.add_question(q) 170 q.ask() 171 return 172 173 def make_service_account(self): 174 sa_name = self.qctx.questions[ServiceAccountName.name].get_answer() 175 sa_namespace = (self.qctx.questions[ServiceAccountNamespace.name] 176 .get_answer()) 177 meta = models.V1ObjectMeta(name=sa_name, namespace=sa_namespace, 178 labels=self.IDENTIFIABLE_LABELS) 179 return models.V1ServiceAccount(api_version="v1", kind="ServiceAccount", 180 metadata=meta) 181 182 def main(self): 183 load_kube_config() 184 self._ask_questions() 185 186 sa_name = self.qctx.questions[ServiceAccountName.name].get_answer() 187 sa_namespace = (self.qctx.questions[ServiceAccountNamespace.name] 188 .get_answer()) 189 190 with fr.Progress(maxval=4, title=TITLE) as p: 191 192 p.info("Creating ServiceAccount...") 193 sa = self.make_service_account() 194 sa = get_or_create_service_account(sa) 195 p.inc() 196 p.success("Created ServiceAccount!") 197 198 p.info("Authorizing ServiceAccount to create tokens for itself...") 199 authorize_token_creation(sa) 200 p.inc() 201 p.success("Authorized ServiceAccount!") 202 203 p.info("Getting a long-lived token for the ServiceAccount...") 204 # We need to wait for the token controller to issue a token and 205 # update the ServiceAccount. Then, we fetch the service account 206 # again and retrieve its token 207 time.sleep(1) 208 token = get_sa_token(get_or_create_service_account(sa)) 209 p.inc() 210 p.success("Retrieved token!") 211 212 p.info("Storing token in file...") 213 file_name = "%s-%s.token" % (sa_namespace, sa_name) 214 mode = stat.S_IRUSR | stat.S_IWUSR 215 with dump_to_file(file_name, mode=mode) as f: 216 f.write(force_unicode(token)) 217 p.inc() 218 219 p.success("Successfully created ServiceAccount `%s/%s' and" 220 " retrieved its token!" 221 % (sa_namespace, sa_name)) 222 223 msg = (u"Successfully retrieved ServiceAccount `%s/%s' token.\n" 224 u"Token is stored in file `%s'." % (sa_namespace, sa_name, 225 file_name)) 226 227 with fr.Message(msg, title=TITLE, lvl=frontend.DEBUG, 228 ok_button=True) as m: 229 m.show() 230 231 232 def main(): 233 cli = ServiceAccountCreator() 234 return cli.run() 235 236 237 if __name__ == "__main__": 238 sys.exit(main()) Alternatively, download the Python file above and upload it to your environment.
Run the script and follow the onscreen instructions. You will be prompted to provide
- the service account name, and
- the namespace it belongs to
$ python3 sa-create.pyFind the long-lived service account token stored in a file named
NAMESPACE-SA.token
.
Option 2: Create Identity Manually¶
Connect to your management environment or a terminal on your notebook server.
Specify the service account name:
$ export NAME=<NAME>Replace
<NAME>
with your service account name, for example:$ export NAME=servingSpecify the namespace of the service account:
$ export NAMESPACE=<NAMESPACE>Replace
<NAMESPACE>
with your namespace, for example:$ export NAMESPACE=kubeflow-userCreate the service account in the desired namespace.
Copy and paste the following Jinja2 template inside
sa.yaml.j2
:sa.yaml.j21 apiVersion: v1 2 kind: ServiceAccount 3 metadata: 4 name: {{ NAME }} 5 namespace: {{ NAMESPACE }} Alternatively, download the file above and upload it to your environment.
Render the manifest:
$ j2 sa.yaml.j2 -o sa.yamlApply the manifest:
$ kubectl apply -f sa.yaml
Create a role with permissions to create service account tokens.
Copy and paste the following Jinja2 template inside
role.yaml.j2
:role.yaml.j21 apiVersion: rbac.authorization.k8s.io/v1 2 kind: Role 3 metadata: 4-11 4 name: {{ NAME }}-create-token 5 namespace: {{ NAMESPACE }} 6 rules: 7 - apiGroups: 8 - "" 9 resourceNames: 10 - {{ NAME }} 11 resources: 12 - serviceaccounts/token 13 verbs: 14 - create Alternatively, download the file above and upload it to your environment.
Render the manifest:
$ j2 role.yaml.j2 -o role.yamlApply the manifest:
$ kubectl apply -f role.yaml
Create a role binding for the created service account and role.
Copy and paste the following Jinja2 template inside
rbac.yaml.j2
:rbac.yaml.j21 apiVersion: rbac.authorization.k8s.io/v1 2 kind: RoleBinding 3 metadata: 4-10 4 name: {{ NAME }}-create-token 5 namespace: {{ NAMESPACE }} 6 roleRef: 7 apiGroup: rbac.authorization.k8s.io 8 kind: Role 9 name: {{ NAME }}-create-token 10 subjects: 11 - kind: ServiceAccount 12 name: {{ NAME }} 13 namespace: {{ NAMESPACE }} Alternatively, download the file above and upload it to your environment.
Render the manifest:
$ j2 rbac.yaml.j2 -o rbac.yamlApply the manifest:
$ kubectl apply -f rbac.yaml
Retrieve the long-lived service account token.
Obtain the service account secret:
$ SECRET=$(kubectl get sa -n ${NAMESPACE?} ${NAME?} -o jsonpath={.secrets[0].name})Obtain the service account token:
$ TOKEN=$(kubectl get secret -n ${NAMESPACE?} ${SECRET?} -o jsonpath={.data.token} | base64 -d)Store the token in a file:
$ echo ${TOKEN?} > ${NAMESPACE?}-${NAME}.tokenView the long-lived token of this service account:
$ echo ${TOKEN?} eyJhbGciOiJSUzI1NiIsIm...