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.

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)

  1. Connect to your management environment.

  2. Copy and paste the following code inside sa-authorize.py:

    sa-authorize.py
    1#!/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
    10import os
    11import sys
    12import enum
    13import yaml
    14import logging
    15
    16from rok_kubernetes import config, models
    17
    18from rok_tasks import frontend, question
    19from rok_tasks.frontend import Choice
    20from rok_tasks.frontend.cli import RokCLI
    21
    22from rok_kubernetes.client import RoleBindingClient, AuthorizationPolicyClient
    23
    24log = logging.getLogger(__name__)
    25fr = frontend.get_frontend()
    26
    27TITLE = "Kubernetes ServiceAccount Authorizer"
    28DESCRIPTION = "Authorize a ServiceAccount in a Namespace"
    29
    30AUTHORIZATION_POLICY = """
    31apiVersion: security.istio.io/v1beta1
    32kind: AuthorizationPolicy
    33metadata:
    34 name: %(name)s
    35 namespace: %(namespace)s
    36spec:
    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
    49def 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
    60def 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
    66def create_authorization_policy(policy):
    67 policy_client = AuthorizationPolicyClient()
    68 namespace = policy["metadata"]["namespace"]
    69 return policy_client.create(policy, namespace=namespace)
    70
    71
    72class 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
    95class 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
    108class 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
    121class 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
    135class 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
    158class 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
    257def main():
    258 cli = ServiceAccountAuthorizer()
    259 return cli.run()
    260
    261
    262if __name__ == "__main__":
    263 sys.exit(main())

    Alternatively, download the Python file above and upload it to your environment.

  3. Run the script and follow the onscreen instructions. You will be prompted to provide

    1. the service account name,
    2. the namespace it belongs to,
    3. the namespace you will grant it access to, and
    4. the desired role in this namespace
    $ python3 sa-authorize.py

    Note

    The role can be view, edit or admin. 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

  1. Connect to your management environment or a terminal on your notebook server.

  2. Specify the service account name:

    $ export NAME=<NAME>

    Replace <NAME> with your service account name, for example:

    $ export NAME=serving
  3. Specify the namespace of the service account:

    $ export NAMESPACE=<NAMESPACE>

    Replace <NAMESPACE> with your namespace, for example:

    $ export NAMESPACE=kubeflow-user
  4. Specify 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-user

    Note

    If you want to able to access services in your namespace, use the same namespace with the one that the service account belongs to.

  5. Specify the desired role for this service account in this namespace:

    $ export ROLE=<ROLE>

    Replace <ROLE> with the desired role, for example:

    $ export ROLE=edit

    Note

    The role can be view, edit or admin. 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.

  6. Create the role binding in the desired namespace.

    1. Copy and paste the following Jinja2 template inside auth-rbac.yaml.j2:

      auth-rbac.yaml.j2
      1apiVersion: rbac.authorization.k8s.io/v1
      2kind: RoleBinding
      3metadata:
      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 }}
      11roleRef:
      12 apiGroup: rbac.authorization.k8s.io
      13 kind: ClusterRole
      14 name: kubeflow-{{ ROLE }}
      15subjects:
      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.

    2. Render the manifest:

      $ j2 auth-rbac.yaml.j2 -o auth-rbac.yaml
    3. Apply the manifest:

      $ kubectl apply -f auth-rbac.yaml
  7. Create the authorization policy in the desired namespace.

    1. Copy and paste the following Jinja2 template inside istio.yaml.j2:

      istio.yaml.j2
      1apiVersion: security.istio.io/v1beta1
      2kind: AuthorizationPolicy
      3metadata:
      4-9
      4 name: {{ NAMESPACE }}-{{ NAME }}
      5 namespace: {{ TARGET_NAMESPACE }}
      6spec:
      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.

    2. Render the manifest:

      $ j2 istio.yaml.j2 -o istio.yaml
    3. Apply 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.