Authorize Identity¶
Once you have created a service account, you may need to give it permissions to perform specific actions. In this section you will authorize an identity to be able to perform actions in a namespace. Specifically, you will create a role binding to grant it permissions. You will make use one of the existing Kubeflow ClusterRoles:
kubeflow-view
kubeflow-edit
kubeflow-admin
You will also create an Istio AuthorizationPolicy to allow authenticated
requests, that is, requests with kubeflow-userid
header matching
system:serviceaccount:NAMESPACE:NAME
.
Choose one of the following options to authorize the identity:
Important
Run this procedure for each namespace you want your service account to have access to.
Overview
What You’ll Need¶
- An existing identity.
- A Python environment with latest Arrikto wheels installed.
- Permissions in order to create the necessary resources (RoleBinding, AuthorizationPolicy) in the desired namespace.
- The Jinja2 command-line tool.
Note
All environment requirements are met inside your management environment. If you don’t have admin permissions in your desired namespace ask your administrator to authorize your service account by running this guide.
Procedure¶
Authorize your existing identity automatically or manually, by choosing one of the following options.
Option 1: Authorize Identity Automatically (preferred)¶
Connect to your management environment.
Copy and paste the following code inside
sa-authorize.py
:sa-authorize.py1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 # 4-260 4 # This file is part of Rok. 5 # 6 # Copyright © 2020-2022 Arrikto Inc. All Rights Reserved. 7 8 """Script to authorize a ServiceAccount for an external client.""" 9 10 import os 11 import sys 12 import enum 13 import yaml 14 import logging 15 16 from rok_kubernetes import config, models 17 18 from rok_tasks import frontend, question 19 from rok_tasks.frontend import Choice 20 from rok_tasks.frontend.cli import RokCLI 21 22 from rok_kubernetes.client import RoleBindingClient, AuthorizationPolicyClient 23 24 log = logging.getLogger(__name__) 25 fr = frontend.get_frontend() 26 27 TITLE = "Kubernetes ServiceAccount Authorizer" 28 DESCRIPTION = "Authorize a ServiceAccount in a Namespace" 29 30 AUTHORIZATION_POLICY = """ 31 apiVersion: security.istio.io/v1beta1 32 kind: AuthorizationPolicy 33 metadata: 34 name: %(name)s 35 namespace: %(namespace)s 36 spec: 37 action: ALLOW 38 rules: 39 - when: 40 - key: request.headers[kubeflow-userid] 41 values: 42 - %(kubeflow_userid)s 43 """ 44 45 # Helpers 46 ###################################################### 47 48 49 def load_kube_config(): 50 # Load K8s configuration, if present 51 if "KUBERNETES_SERVICE_HOST" in os.environ: 52 config.load_incluster_config() 53 else: 54 try: 55 config.load_kube_config() 56 except IOError as e: 57 raise RuntimeError("No K8s configuration was found: %s" % e) 58 59 60 def create_role_binding(rb): 61 namespace = rb.metadata.namespace 62 role_binding_client = RoleBindingClient() 63 return role_binding_client.create(rb, namespace=namespace) 64 65 66 def create_authorization_policy(policy): 67 policy_client = AuthorizationPolicyClient() 68 namespace = policy["metadata"]["namespace"] 69 return policy_client.create(policy, namespace=namespace) 70 71 72 class Role(enum.Enum): 73 """Enumaration of supported roles.""" 74 75 VIEW = "view" 76 EDIT = "edit" 77 ADMIN = "admin" 78 79 @property 80 def desc(self): 81 if self is self.VIEW: 82 return "Viewer" 83 elif self is self.EDIT: 84 return "Editor" 85 elif self is self.ADMIN: 86 return "Administrator" 87 else: 88 raise RuntimeError("Invalid role option: %s" % self) 89 90 91 # Questions 92 ###################################################### 93 94 95 class ServiceAccountName(question.LineInputQuestion): 96 """Question for Service Account Name.""" 97 98 name = "question/service_account_name" 99 msg = "What is the name of the ServiceAccount?" 100 required = True 101 argument_opts = { 102 "name": ["--service-account-name"], 103 "metavar": "SERVICE_ACCOUNT_NAME", 104 "help": "Use ServiceAccount with name %(metavar)s" 105 } 106 107 108 class ServiceAccountNamespace(question.LineInputQuestion): 109 """Question for Service Account Namespace.""" 110 111 name = "question/service_account_namespace" 112 msg = "What is the namespace of the ServiceAccount?" 113 required = True 114 argument_opts = { 115 "name": ["--service-account-namespace"], 116 "metavar": "SERVICE_ACCOUNT_NAMESPACE", 117 "help": "Use ServiceAccount with namespace %(metavar)s" 118 } 119 120 121 class Role(question.MenuQuestion): 122 """Question for Role.""" 123 124 name = "question/role" 125 msg = "What is the role you wish to grant to the ServiceAccount?" 126 required = True 127 argument_opts = { 128 "name": ["--role"], 129 "metavar": "ROLE", 130 "help": "Bind ServiceAccount with role %(metavar)s" 131 } 132 choices = [Choice(id=r.value, desc=r.desc, long_desc="") for r in Role] 133 134 135 class Namespace(question.LineInputQuestion): 136 """Question for the Namespace to give access to.""" 137 138 def __init__(self, *args, **kwargs): 139 super(Namespace, self).__init__(*args, **kwargs) 140 sa_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 141 if os.path.exists(sa_path): 142 with open(sa_path) as f: 143 self.default = f.readline() 144 145 name = "question/namespace" 146 msg = "What is the namespace you want to give access to?" 147 required = True 148 argument_opts = { 149 "name": ["--namespace"], 150 "help": "Namespace to give access to" 151 } 152 153 154 # CLI 155 ###################################################### 156 157 158 class ServiceAccountAuthorizer(RokCLI): 159 """Create a ServiceAccount and ServiceAccountToken.""" 160 161 IDENTIFIABLE_LABELS = {"arrikto.com/service-account-token-gen": ""} 162 DEFAULT_LOGFILE = "service-account-authorizer.log" 163 DEFAULT_FRONTEND = "readline" 164 165 def initialize_parser(self): 166 parser = super(ServiceAccountAuthorizer, self).initialize_parser() 167 questions = [ServiceAccountName(), ServiceAccountNamespace(), Role(), 168 Namespace()] 169 self.questions = questions 170 question.add_questions_args(questions, parser) 171 return parser 172 173 def _ask_questions(self): 174 # TODO: Ask all questions 175 for q in self.questions: 176 self.qctx.add_question(q) 177 q.ask() 178 return 179 180 def make_role_binding(self): 181 sa_name = self.qctx.questions[ServiceAccountName.name].get_answer() 182 sa_namespace = (self.qctx.questions[ServiceAccountNamespace.name] 183 .get_answer()) 184 role = self.qctx.questions[Role.name].get_answer() 185 namespace = self.qctx.questions[Namespace.name].get_answer() 186 187 kubeflow_role = "kubeflow-%s" % role 188 name = "%s-%s-%s" % (sa_namespace, sa_name, role) 189 user_name = "system:serviceaccount:%s:%s" % (sa_namespace, sa_name) 190 191 metadata = models.V1ObjectMeta(annotations={"user": user_name, 192 "role": role}, 193 name=name, namespace=namespace, 194 labels=self.IDENTIFIABLE_LABELS) 195 role_ref = models.V1RoleRef(api_group="rbac.authorization.k8s.io", 196 kind="ClusterRole", name=kubeflow_role) 197 subject = models.V1Subject(api_group="rbac.authorization.k8s.io", 198 kind="User", name=user_name) 199 return models.V1RoleBinding(api_version="rbac.authorization.k8s.io/v1", 200 kind="RoleBinding", metadata=metadata, 201 role_ref=role_ref, subjects=[subject]) 202 203 def make_authorization_policy(self): 204 sa_name = self.qctx.questions[ServiceAccountName.name].get_answer() 205 sa_namespace = (self.qctx.questions[ServiceAccountNamespace.name] 206 .get_answer()) 207 namespace = self.qctx.questions[Namespace.name].get_answer() 208 209 name = "%s-%s" % (sa_namespace, sa_name) 210 kubeflow_userid = "system:serviceaccount:%s:%s" % (sa_namespace, 211 sa_name) 212 policy = AUTHORIZATION_POLICY % {"name": name, 213 "namespace": namespace, 214 "kubeflow_userid": kubeflow_userid} 215 policy = yaml.safe_load(policy) 216 return policy 217 218 def main(self): 219 load_kube_config() 220 self._ask_questions() 221 222 sa_name = self.qctx.questions[ServiceAccountName.name].get_answer() 223 sa_namespace = (self.qctx.questions[ServiceAccountNamespace.name] 224 .get_answer()) 225 role = self.qctx.questions[Role.name].get_answer() 226 namespace = self.qctx.questions[Namespace.name].get_answer() 227 228 with fr.Progress(maxval=4, title=TITLE) as p: 229 230 p.info("Generating RoleBinding...") 231 rb = self.make_role_binding() 232 p.inc() 233 234 p.info("Applying RoleBinding...") 235 create_role_binding(rb) 236 p.inc() 237 238 p.info("Generating AuthorizationPolicy...") 239 policy = self.make_authorization_policy() 240 p.inc() 241 242 p.info("Applying AuthorizationPolicy...") 243 create_authorization_policy(policy) 244 p.inc() 245 246 p.success("Successfully created necessary resources") 247 248 msg = (u"Successfully granted `%s' access in namespace `%s' to the" 249 u" ServiceAccount `%s/%s'.\n" 250 % (role, namespace, sa_namespace, sa_name)) 251 252 with fr.Message(msg, title=TITLE, lvl=frontend.DEBUG, 253 ok_button=True) as m: 254 m.show() 255 256 257 def main(): 258 cli = ServiceAccountAuthorizer() 259 return cli.run() 260 261 262 if __name__ == "__main__": 263 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,
- the namespace it belongs to,
- the namespace you will grant it access to, and
- the desired role in this namespace
$ python3 sa-authorize.pyNote
The role can be
view
,edit
oradmin
. These map with the corresponding Kubeflow ClusterRoles. This is required for services that do SubjectAccessReview on each request, for example Rok or KFP. For other services like inference services, the role is irrelevant, but you still need to create an Istio AuthorizationPolicy, so go on with this guide.
Option 2: Authorize 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-userSpecify the namespace you will grant the service account access to:
$ export TARGET_NAMESPACE=<NAMESPACE>Replace
<NAMESPACE>
with your namespace, for example:$ export TARGET_NAMESPACE=kubeflow-userNote
If you want to able to access services in your namespace, use the same namespace with the one that the service account belongs to.
Specify the desired role for this service account in this namespace:
$ export ROLE=<ROLE>Replace
<ROLE>
with the desired role, for example:$ export ROLE=editNote
The role can be
view
,edit
oradmin
. These map with the corresponding Kubeflow ClusterRoles. This is required for services that do SubjectAccessReview on each request, for example Rok or KFP. For other services like inference services, the role is irrelevant, but you still need to create an Istio AuthorizationPolicy, so go on with this guide.Create the role binding in the desired namespace.
Copy and paste the following Jinja2 template inside
auth-rbac.yaml.j2
:auth-rbac.yaml.j21 apiVersion: rbac.authorization.k8s.io/v1 2 kind: RoleBinding 3 metadata: 4-15 4 annotations: 5 role: {{ ROLE }} 6 user: system:serviceaccount:{{ NAMESPACE }}:{{ NAME }} 7 labels: 8 arrikto.com/service-account-token-gen: "" 9 name: {{ NAMESPACE }}-{{ NAME }}-{{ ROLE }} 10 namespace: {{ TARGET_NAMESPACE }} 11 roleRef: 12 apiGroup: rbac.authorization.k8s.io 13 kind: ClusterRole 14 name: kubeflow-{{ ROLE }} 15 subjects: 16 - apiGroup: rbac.authorization.k8s.io 17 kind: User 18 name: system:serviceaccount:{{ NAMESPACE }}:{{ NAME }} Alternatively, download the file above and upload it to your environment.
Render the manifest:
$ j2 auth-rbac.yaml.j2 -o auth-rbac.yamlApply the manifest:
$ kubectl apply -f auth-rbac.yaml
Create the authorization policy in the desired namespace.
Copy and paste the following Jinja2 template inside
istio.yaml.j2
:istio.yaml.j21 apiVersion: security.istio.io/v1beta1 2 kind: AuthorizationPolicy 3 metadata: 4-9 4 name: {{ NAMESPACE }}-{{ NAME }} 5 namespace: {{ TARGET_NAMESPACE }} 6 spec: 7 action: ALLOW 8 rules: 9 - when: 10 - key: request.headers[kubeflow-userid] 11 values: 12 - system:serviceaccount:{{ NAMESPACE }}:{{ NAME }} Alternatively, download the file above and upload it to your environment.
Render the manifest:
$ j2 istio.yaml.j2 -o istio.yamlApply the manifest:
$ kubectl apply -f istio.yaml
Summary¶
You have successfully granted an identity (service account) access in your desired namespace.
What’s Next¶
The next step is to create a short-lived token using this identity and use it to authenticate an external client.