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:

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)

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

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

    sa-create.py
    1#!/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
    10This script aids on getting or creating a ServiceAccount in a specific
    11namespace and returns its long-lived token. The token can be used in an
    12external client for authentication purposes.
    13"""
    14
    15import os
    16import sys
    17import stat
    18import time
    19import base64
    20import logging
    21
    22from rok_kubernetes import config, models
    23from rok_kubernetes.exceptions import ApiException
    24
    25from rok_common.encoding import force_unicode
    26from rok_common.fileutils import dump_to_file
    27
    28from rok_kubernetes.client import (RoleClient, SecretClient, RoleBindingClient,
    29 ServiceAccountClient)
    30
    31from rok_tasks import frontend, question
    32from rok_tasks.frontend.cli import RokCLI
    33
    34log = logging.getLogger(__name__)
    35fr = frontend.get_frontend()
    36
    37TITLE = "Kubernetes ServiceAccount Creator"
    38DESCRIPTION = "Generate a ServiceAccount on Kubernetes"
    39
    40# Questions
    41######################################################
    42
    43
    44class 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
    57class 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
    81def 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
    92def 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
    106def 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
    117def 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
    129def 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
    140def 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
    153class 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
    232def main():
    233 cli = ServiceAccountCreator()
    234 return cli.run()
    235
    236
    237if __name__ == "__main__":
    238 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, and
    2. the namespace it belongs to
    $ python3 sa-create.py
  4. Find the long-lived service account token stored in a file named NAMESPACE-SA.token.

Option 2: Create 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. Create the service account in the desired namespace.

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

      sa.yaml.j2
      1apiVersion: v1
      2kind: ServiceAccount
      3metadata:
      4 name: {{ NAME }}
      5 namespace: {{ NAMESPACE }}

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

    2. Render the manifest:

      $ j2 sa.yaml.j2 -o sa.yaml
    3. Apply the manifest:

      $ kubectl apply -f sa.yaml
  5. Create a role with permissions to create service account tokens.

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

      role.yaml.j2
      1apiVersion: rbac.authorization.k8s.io/v1
      2kind: Role
      3metadata:
      4-11
      4 name: {{ NAME }}-create-token
      5 namespace: {{ NAMESPACE }}
      6rules:
      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.

    2. Render the manifest:

      $ j2 role.yaml.j2 -o role.yaml
    3. Apply the manifest:

      $ kubectl apply -f role.yaml
  6. Create a role binding for the created service account and role.

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

      rbac.yaml.j2
      1apiVersion: rbac.authorization.k8s.io/v1
      2kind: RoleBinding
      3metadata:
      4-10
      4 name: {{ NAME }}-create-token
      5 namespace: {{ NAMESPACE }}
      6roleRef:
      7 apiGroup: rbac.authorization.k8s.io
      8 kind: Role
      9 name: {{ NAME }}-create-token
      10subjects:
      11- kind: ServiceAccount
      12 name: {{ NAME }}
      13 namespace: {{ NAMESPACE }}

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

    2. Render the manifest:

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

      $ kubectl apply -f rbac.yaml
  7. Retrieve the long-lived service account token.

    1. Obtain the service account secret:

      $ SECRET=$(kubectl get sa -n ${NAMESPACE?} ${NAME?} -o jsonpath={.secrets[0].name})
    2. Obtain the service account token:

      $ TOKEN=$(kubectl get secret -n ${NAMESPACE?} ${SECRET?} -o jsonpath={.data.token} | base64 -d)
    3. Store the token in a file:

      $ echo ${TOKEN?} > ${NAMESPACE?}-${NAME}.token
    4. View the long-lived token of this service account:

      $ echo ${TOKEN?} eyJhbGciOiJSUzI1NiIsIm...

Summary

You have successfully created an identity (service account) in your desired namespace and obtained its long-lived token.

What’s Next

The next step is to authorize the identity you created.