Renew Rok Registry Certificate for Rok Clusters

Rok Registry issues a certificate for every Rok cluster that has been registered with. The Rok sync daemons use these certificates to authenticate against each other before starting syncing buckets. The Rok Registry issues certificates with a specific validity period of one year.

Note

There is currently no automated mechanism to renew the certificates, so you have to renew them manually. We will support an automated mechanism in a future release.

This section will guide you through renewing the certificates that Rok Registry granted for your Rok clusters.

What You’ll Need

Procedure

  1. Go to the management environment of the Rok Registry.

    Note

    Keep an open shell for that Rok Registry to use it throughout the guide.

  2. Validate that you are in the right management environment by making sure you can find the RokRegistryCluster CR:

    root@rok-tools:~# kubectl get rokregistrycluster -n rok-registry NAME VERSION HEALTH TOTAL MEMBERS READY MEMBERS PHASE AGE rok-registry l0-release-v1.1-pre-2-g6b0b76380 OK 1 1 Running 42m
  3. Download the Python scripts you will use later to manage your certificates:

    Note

    In the following scripts, we refer to Rok clusters as Rok applications.

    1. Download the app_list.py script provided below and store it in your /root directory.

      app_list.py
      1#!/usr/bin/env python2
      2# -*- coding: utf-8 -*-
      3#
      4# This file is part of Rok.
      5#
      6# Copyright © 2022 Arrikto Inc. All Rights Reserved.
      7
      8"""Helper script to list the applications in Rok Registry."""
      9-191
      9
      10
      11import os
      12import sys
      13import pytz
      14from django.db import connection
      15from django.conf import settings
      16
      17from rok_common import printutils
      18from rok_common.crypto import encodings
      19from rok_common.crypto.certificates import Certificate
      20from rok_tasks import frontend, question
      21from rok_tasks.frontend.cli import RokCLI
      22
      23
      24fr = frontend.get_frontend()
      25
      26TITLE = "List Applications in Rok Registry"
      27
      28# Questions
      29######################################################
      30
      31
      32class UserNameQuestion(question.LineInputQuestion):
      33 """The name of the Rok Registry user."""
      34
      35 title = TITLE + ": Username"
      36 name = "question/user"
      37 msg = "What is the name of the Rok Registry user?"
      38 desc = ("The name of the Rok Registry user to obtain the applications for."
      39 " Leave empty to select all users.")
      40 envvars = "ROK_REGISTRY_USERNAME"
      41 argument_opts = {
      42 "name": ["--user"],
      43 "metavar": "USER",
      44 "help": ("List applications of user %(metavar)s. Leave empty to select"
      45 " all users.")
      46 }
      47 default = ""
      48 required = True
      49 allow_empty = True
      50 priority = question.LOW
      51
      52# Helpers
      53######################################################
      54
      55
      56def print_table(objects, headers, obj_attrs):
      57 """Print the table for the Indexer applications."""
      58 table = printutils.create_horizontal_table(headers, header=True,
      59 border=True)
      60 for obj in objects:
      61 table.add_row([obj[attr] for attr in obj_attrs])
      62 msg = "\n" + str(table)
      63 with fr.Message(msg, title=TITLE, lvl=frontend.INFO) as m:
      64 m.show()
      65
      66# CLI
      67######################################################
      68
      69
      70class ApplicationListCLI(RokCLI):
      71 """CLI tool to list the applications in Rok Registry."""
      72
      73 DESCRIPTION = "List the applications in Rok Registry."
      74
      75 # XXX: The dialog frontend uses fixed dimensions for the messagebox, so it
      76 # cannot print a wide table without wrapping. Therefore, make the readline
      77 # the default frontend for now.
      78 DEFAULT_FRONTEND = "readline"
      79 DEFAULT_LOGFILE = "indexer-app-list.log"
      80
      81 def initialize_parser(self):
      82 parser = super(ApplicationListCLI, self).initialize_parser()
      83 questions = [UserNameQuestion()]
      84 question.add_questions_args(questions, parser)
      85 return parser
      86
      87 def ask_questions(self):
      88 questions = [UserNameQuestion()]
      89 for q in questions:
      90 self.qctx.add_question(q)
      91 q.ask()
      92
      93 @classmethod
      94 def get_app_url(cls, app):
      95 """Get the URL of an Indexer app."""
      96 # FIXME: Ideally, we should obtain the application's URL from the
      97 # 'website_url' field of the underlying Fort app.
      98 #
      99 # However, the current situation for Rok clusters is that this field
      100 # points to the URL of the internal Kubernetes service. See also:
      101 # https://github.com/arrikto/rok/issues/3828
      102 #
      103 # Use the redirect URI as a temporary workaround.
      104 redirect_uris = app["redirect_uris"].split()
      105 return str(redirect_uris[0].split("/")[-4])
      106
      107 @classmethod
      108 def format_datetime(cls, datetime):
      109 """Format a datetime object into human-readable form."""
      110 if not datetime.tzinfo:
      111 datetime = datetime.replace(tzinfo=pytz.UTC)
      112 return datetime.strftime("%Y-%m-%d %H:%M:%S %Z")
      113
      114 @classmethod
      115 def print_apps(cls, indx_apps):
      116 """Print a list of Indexer apps."""
      117 # Set up attributes for printing.
      118 for app in indx_apps:
      119 app["url"] = cls.get_app_url(app)
      120 if app["certificate"]:
      121 try:
      122 cert = Certificate.load(str(app["certificate"]),
      123 encoding=encodings.PEM_NO_NEWLINES)
      124 except ValueError:
      125 msg = ("Failed to load the certificate for application"
      126 " `%s'." % app["client_id"])
      127 raise ValueError(msg)
      128 app["certificate_expires_at"] = \
      129 cls.format_datetime(cert.expiration_date)
      130 else:
      131 # No certificate exists for this app. Reserve the empty string
      132 # for this situation. Note that certificates with infinite
      133 # duration are not allowed, so probably this won't cause any
      134 # confusion.
      135 app["certificate_expires_at"] = ""
      136 # Re-format and save registration date in new attribute as Fort
      137 # attributes are read-only.
      138 app["_created_at"] = cls.format_datetime(app["created_at"])
      139
      140 # Print table.
      141 headers = ["CLIENT ID", "CLIENT URL", "USER", "CLIENT REGISTERED AT",
      142 "CERTIFICATE EXPIRES AT"]
      143 obj_attrs = ["client_id", "url", "username", "_created_at",
      144 "certificate_expires_at"]
      145 print_table(indx_apps, headers, obj_attrs)
      146
      147 def main(self):
      148 """List the applications in Rok Registry."""
      149 self.ask_questions()
      150 account_name = self.qctx.questions[UserNameQuestion.name].get_answer()
      151
      152 with connection.cursor() as cursor:
      153 # FIXME: We make the assumption here that the Indexer and Fort are
      154 # using the same database. The ideal would be to find Fort's
      155 # database from its own django settings, but currently we cannot
      156 # import them from Python2.
      157 query = ("WITH indx_apps AS ( "
      158 " SELECT * "
      159 " FROM rok_fort_api_application "
      160 " NATURAL JOIN rok_indexer_api_applicationmodel) "
      161 "SELECT indx_apps.*, "
      162 " rok_fort_api_account.display_name as username "
      163 "FROM indx_apps "
      164 "INNER JOIN rok_fort_api_account "
      165 "ON indx_apps.user_id = rok_fort_api_account.uuid")
      166 cursor.execute(query)
      167 columns = [col[0] for col in cursor.description]
      168 indx_apps = [dict(zip(columns, row)) for row in cursor.fetchall()]
      169
      170 if account_name:
      171 # Filter out the Indexer applications that do not belong to the
      172 # provided user.
      173 with connection.cursor() as cursor:
      174 cursor.execute("SELECT uuid FROM rok_fort_api_account WHERE"
      175 " display_name = %s", [account_name])
      176 result = cursor.fetchall()
      177 if not result:
      178 raise ValueError("Invalid user `%s'." % account_name)
      179 account_uuid = result[0][0]
      180 indx_apps = filter(lambda app: app["user_id"] == account_uuid,
      181 indx_apps)
      182
      183 # Print the Indexer apps.
      184 self.print_apps(indx_apps)
      185
      186
      187def main():
      188 # Load django settings according to:
      189 # https://docs.djangoproject.com/en/2.1/topics/settings/#either-configure-or-django-settings-module-is-required # noqa: E501
      190 if not settings.configured and "DJANGO_SETTINGS_MODULE" not in os.environ:
      191 os.environ.setdefault("DJANGO_SETTINGS_MODULE",
      192 "rok_indexer_prj.settings")
      193
      194 cli = ApplicationListCLI()
      195 return cli.run()
      196
      197
      198if __name__ == "__main__":
      199 sys.exit(main())

      You can download the app_list.py script and store it in your /root directory as such:

      root@rok-tools:~# wget -O /root/app_list.py https://docs.arrikto.com/_downloads/app_list.py
    2. Download the cert_delete.py script provided below and store it in your /root directory.

      cert_delete.py
      1#!/usr/bin/env python2
      2# -*- coding: utf-8 -*-
      3#
      4# This file is part of Rok.
      5#
      6# Copyright © 2022 Arrikto Inc. All Rights Reserved.
      7
      8"""Helper script to delete the certiticate of a Rok Registry application."""
      9-133
      9
      10
      11import os
      12import sys
      13import django
      14
      15from django.conf import settings
      16from django.db import transaction
      17from django.utils.timezone import now
      18
      19from rok_tasks import frontend, question
      20from rok_tasks.frontend.cli import RokCLI
      21
      22
      23fr = frontend.get_frontend()
      24
      25TITLE = "Delete Certificate of Rok Registry Application"
      26
      27# Questions
      28######################################################
      29
      30
      31class ClientIDQuestion(question.LineInputQuestion):
      32 """Question for Client ID."""
      33
      34 title = TITLE + ": Client ID"
      35 name = "question/client_id"
      36 msg = "What is the client ID of the Rok Registry application?"
      37 desc = "The client ID of the Rok Registry application."
      38 envvars = "ROK_REGISTRY_CLIENT_ID"
      39 argument_opts = {
      40 "name": ["--client-id"],
      41 "metavar": "CLIENT_ID",
      42 "help": desc
      43 }
      44 required = True
      45 allow_empty = False
      46
      47
      48class ConfirmDeletionQuestion(question.TrueOrFalseQuestion):
      49 """Question to confirm deleting the certificate."""
      50
      51 title = TITLE + ": Confirm"
      52 name = "question/confirm_deletion"
      53 desc = ("Are you sure you want to delete the certificate of the following"
      54 " application?")
      55 envvars = "ROK_REGISTRY_DELETE_CERT"
      56 default = False
      57 true = frontend.Choice(id="true", desc="Delete certificate")
      58 false = frontend.Choice(id="false", desc="Exit")
      59 no_cancel = True
      60 no_help = True
      61 argument_opts = {
      62 "name": ["--confirm-deletion"],
      63 "help": "Delete the certificate of the provided application."
      64 }
      65 required = True
      66 allow_empty = False
      67
      68 def __init__(self, client_id, **kwargs):
      69 msg = "\n" + client_id
      70 super(ConfirmDeletionQuestion, self).__init__(msg=msg, **kwargs)
      71
      72# CLI
      73######################################################
      74
      75
      76class CertDeleteCLI(RokCLI):
      77 """CLI tool to delete the certificate of a Rok Registry application."""
      78
      79 DESCRIPTION = "Delete the certificate of a Rok Registry application."""
      80
      81 DEFAULT_FRONTEND = "readline"
      82 DEFAULT_LOGFILE = "indexer-cert-delete.log"
      83
      84 def initialize_parser(self):
      85 parser = super(CertDeleteCLI, self).initialize_parser()
      86 questions = [ClientIDQuestion(),
      87 ConfirmDeletionQuestion(client_id="")]
      88 question.add_questions_args(questions, parser)
      89 return parser
      90
      91 def ask_questions(self):
      92 questions = [ClientIDQuestion()]
      93 for q in questions:
      94 self.qctx.add_question(q)
      95 q.ask()
      96
      97 @classmethod
      98 def delete_cert(cls, client_id):
      99 # We make this import here instead of top-level because django.setup()
      100 # must have been called previously.
      101 #
      102 # See also:
      103 # https://docs.djangoproject.com/en/2.1/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage # noqa: E501
      104 from rok_indexer_api.logic.apps import get_app
      105 with transaction.atomic():
      106 app = get_app(client_id, lock=True)
      107 app.certificate = ""
      108 app.certificate_fingerprint = ""
      109 app.certificate_created_at = now()
      110 app.save()
      111
      112 def main(self):
      113 """Delete the certificate of a Rok Registry application."""
      114 self.ask_questions()
      115 client_id = self.qctx.questions[ClientIDQuestion.name].get_answer()
      116 confirm_q = ConfirmDeletionQuestion(client_id=client_id)
      117 self.qctx.add_question(confirm_q)
      118 if not confirm_q.ask():
      119 raise frontend.UserCanceledInputError
      120
      121 with fr.Progress(maxval=1, title=TITLE) as p:
      122 p.info("Deleting the certificate of the Rok Registry"
      123 " application...")
      124 self.delete_cert(client_id)
      125 p.success("Successfully deleted the certificate of the Rok"
      126 " Registry application with client ID `%s'." % client_id)
      127
      128
      129def main():
      130 # https://docs.djangoproject.com/en/2.1/topics/settings/#either-configure-or-django-settings-module-is-required # noqa: E501
      131 if not settings.configured and "DJANGO_SETTINGS_MODULE" not in os.environ:
      132 os.environ.setdefault("DJANGO_SETTINGS_MODULE",
      133 "rok_indexer_prj.settings")
      134 django.setup()
      135
      136 cli = CertDeleteCLI()
      137 return cli.run()
      138
      139
      140if __name__ == "__main__":
      141 sys.exit(main())

      You can download the cert_delete.py script and store it in your /root directory as such:

      root@rok-tools:~# wget -O /root/cert_delete.py https://docs.arrikto.com/_downloads/cert_delete.py
  4. List all Rok clusters that have been registered with Rok Registry:

    root@rok-tools:~# kubectl exec -ti \ > svc/rok-registry \ > -n rok-registry -- \ > python2 -c "$(cat /root/app_list.py)" List Applications in Rok Registry: -------------------------------------------------------------------------------------------------------------------------------------------- CLIENT ID CLIENT URL USER CLIENT REGISTERED AT CERTIFICATE EXPIRES AT -------------------------------------------------------------------------------------------------------------------------------------------- wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhs arrikto-cluster-1.apps.example.com user 2022-03-10 16:48:33 UTC 2023-03-10 16:48:37 UTC U5sWM4yGT09hW95Se5MS6zP13W07TQDc1n8C2lhO arrikto-cluster-2.apps.example.com user 2022-08-17 16:50:32 UTC 2023-08-17 16:50:46 UTC 84IDEyNyePoRXAUu9zIJGRSX6hXfgKc2AMB0bR67 arrikto-cluster-3.apps.example.com user 2022-10-25 16:31:14 UTC 2023-10-25 16:32:10 UTC --------------------------------------------------------------------------------------------------------------------------------------------
  5. Check the certificate expiration dates of all Rok clusters from the previous step and select those for which you want to renew their certificates. Repeat steps a-j below for each one of your selected Rok clusters to renew their certificates:

    Note

    We suggest to renew all certificates at once so that you can schedule this process only once per year.

    1. Set the client ID of the Rok cluster:

      root@rok-tools:~# export CLIENT_ID=<APP_ID>

      Replace the <APP_ID> with the client ID of the desired Rok cluster, for example:

      root@rok-tools:~# export CLIENT_ID=wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhs
    2. Delete the certificate of the Rok cluster from the Rok Registry:

      root@rok-tools:~# kubectl exec -ti \ > svc/rok-registry \ > -n rok-registry -- \ > python2 -c "$(cat /root/cert_delete.py)" \ > --client-id ${CLIENT_ID?} \ > --confirm-deletion Success: Successfully deleted the certificate of the Rok Registry application.
    3. Create an one-hour token for that Rok cluster:

      root@rok-tools:~# export APP_TOKEN=$(kubectl exec \ > -it \ > -n rok-registry \ > svc/rok-registry \ > -- \ > rok-fort-manage token-create \ > --app ${CLIENT_ID} \ > --duration 3600 \ > --no-borders \ > | grep Token \ > | awk '{print $2}') && \ > echo $APP_TOKEN gIZtj9N8j5EK5z8dqsYcEDqjszCeJd
    4. Go to the management environment of your Rok cluster. Use the client URL from step 4 to determine the Rok cluster.

    5. Validate that you are in the right management environment:

      1. Obtain the subdomain of the Rok cluster:

        root@rok-tools:~# kubectl get ingress istio-ingress \ > -n istio-system \ > -ojson \ > | jq -r '.spec.rules[0].host' arrikto-cluster-1.apps.example.com
      2. Make sure that the subdomain from the previous step matches the URL from step 4.

    6. Set the client ID and the token of your Rok cluster:

      root@rok-tools:~# export CLIENT_ID=<APP_ID>
      root@rok-tools:~# export APP_TOKEN=<TOKEN>

      Replace <APP_ID> and <TOKEN> with the client ID and the token for your Rok cluster, for example:

      root@rok-tools:~# export CLIENT_ID=wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhs
      root@rok-tools:~# export APP_TOKEN=gIZtj9N8j5EK5z8dqsYcEDqjszCeJd

      Important

      These values must match the CLIENT_ID and the APP_TOKEN of the step 5a and step 5c, respectively.

    7. Validate that this Rok cluster uses the provided client ID:

      root@rok-tools:~# kubectl exec -it \ > -n rok \ > svc/rok-etcd \ > -- \ > env ETCDCTL_API=3 \ > etcdctl get \ > --prefix /rok/rokgw/indexers/ \ > --print-value-only \ > | jq -e ". | select(.client_id == \"${CLIENT_ID?}\")" \ > >/dev/null && echo TRUE || echo FALSE TRUE

      Troubleshooting

      The output of the command is FALSE

      If the output of the above command is FALSE, it means that this Rok cluster no longer uses the provided client ID, that is, the corresponding OAuth client in Rok Registry is stale.

      This can happen if you have de-registered the Rok cluster from Rok Registry, but you did not delete the corresponding OAuth client from Rok Registry.

      To proceed, run the following steps:

      1. Switch back to the management environment for Rok Registry.

      2. Optional

        Delete the stale OAuth client from Rok Registry:

        root@rok-tools:~# kubectl exec \ > -it \ > -n rok-registry \ > svc/rok-registry \ > -- \ > rok-fort-manage app-delete ${CLIENT_ID?} The application to be deleted is the following: +---------------+------------------------------------------------------------------------------+ | Client ID | wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhs | | Type | oauth | | Owner UUID | 8b7898ef-4e80-4059-a1a2-638e289c9879 | | Name | Rok 2.0-pre-808-g1e4439088 (Aurora) | | Description | RokE@rok.rok.svc.cluster.local | | Redirect URIs | https://arrikto-cluster-1.apps.example.com/rok/oauth-callback/indx_suh9u9jz2 | | Website URL | https://rok.rok.svc.cluster.local | | Created at | 2022-10-17T16:48:33.864782+00:00 | | Updated at | 2022-10-17T16:48:33.864788+00:00 | +---------------+------------------------------------------------------------------------------+ Are you sure you want to delete it? [y/N]: y Application deleted.
      3. Continue from step 5a to renew the certificates for the remaining of your selected Rok clusters.

    8. Create a new certificate for the Rok cluster:

      1. Download the cert_renew.py script provided below and store it in your /root directory.

        cert_renew.py
        1#!/usr/bin/env python2
        2# -*- coding: utf-8 -*-
        3#
        4# This file is part of Rok.
        5#
        6# Copyright © 2022 Arrikto Inc. All Rights Reserved.
        7#
        8
        9-249
        9"""Script to create a new cerificate for a Rok Application.
        10
        11This Rok Application should be registered with a Rok Registry.
        12"""
        13
        14import os
        15import sys
        16import json
        17import django
        18from django.conf import settings
        19
        20from rok_gw import backend as gw_backend
        21from rok_gw_api import cache as gw_cache
        22from rok_common.crypto import certificates, encodings
        23from rok_common import protect, printutils, http as rok_http
        24from rok_django_lib.management import get_field
        25
        26from rok_tasks import frontend, question
        27from rok_tasks.frontend.cli import RokCLI
        28
        29
        30fr = frontend.get_frontend()
        31
        32TITLE = "Create a new Certificate for a registered Rok Application"
        33DESCRIPTION = "Create a new Certificate for a registered Rok Application"
        34
        35# Questions
        36######################################################
        37
        38
        39class ClientID(question.LineInputQuestion):
        40 """Question for Client ID."""
        41
        42 title = TITLE + ": Client ID"
        43 name = "question/client_id"
        44 msg = "What is the client ID of the registered Rok Application?"
        45 desc = "The ID of the Rok Application to renew the certificate for."
        46 envvars = "ROK_GW_CLIENT_ID"
        47 argument_opts = {
        48 "name": ["--client-id"],
        49 "metavar": "CLIENT_ID",
        50 "help": desc
        51 }
        52 required = True
        53 allow_empty = False
        54
        55
        56class RokRegistryAppToken(question.VerifyPasswordInputQuestion):
        57 """Question for the Rok Registry App token for Fort."""
        58
        59 title = TITLE + ": Token"
        60 name = "question/rok_registry_app_token"
        61 msg = "What is the token of the registered Rok Application?"
        62 verify_msg = "Re-enter the token to verify"
        63 desc = ("The token to authenticate the certificate signing request against"
        64 " the Rok Registry.")
        65 envvars = "ROK_GW_TOKEN"
        66 argument_opts = {
        67 "name": ["--token"],
        68 "metavar": "TOKEN",
        69 "help": desc
        70 }
        71 required = True
        72 allow_empty = False
        73
        74# Helpers
        75######################################################
        76
        77
        78def get_resp_msg(resp):
        79 """Get the message from an error response."""
        80 resp_msg = None
        81 api_error = resp.json()
        82 if isinstance(api_error, dict):
        83 _, error = list(api_error.items())[0]
        84 resp_msg = error["message"]
        85 return resp_msg
        86
        87
        88def handle_40x_status(resp):
        89 """Log proper messages to help users make a valid CSR request."""
        90 if resp.status_code == 401:
        91 msg = (u"Authorization failed. Ensure that the token for the"
        92 u" registered Rok Application is valid and that it has not"
        93 u" expired.\n")
        94 with fr.Message(msg, title=TITLE, lvl=frontend.ERROR,
        95 ok_button=True) as m:
        96 m.show()
        97 elif resp.status_code == 403:
        98 # This error could mean a lot of things. Parse the response message to
        99 # find out.
        100 resp_msg = get_resp_msg(resp)
        101
        102 # Expected error messages from Rok Registry.
        103 not_registered = (
        104 u"Only registered applications can request a certificate")
        105 wrong_app = u"Cannot create certificates for other applications"
        106 cert_exists = u"Replacing certificates is not implemented yet"
        107 # Corresponding error messages to expose to the user.
        108 not_registered_msg = (
        109 u"Application not registered. Make sure the provided token belongs"
        110 u" to your application and your application is registered with Rok"
        111 u" Registry.\n")
        112 wrong_app_msg = (
        113 u"Mismatch between client ID and token. Make sure the provided"
        114 u" token belongs to your desired Rok application.\n")
        115 cert_exists_msg = (
        116 u"A certificate already exists. Ensure that you have deleted the"
        117 u" old certificate of this application from Rok Registry.\n")
        118 out_msg = {not_registered: not_registered_msg,
        119 wrong_app: wrong_app_msg,
        120 cert_exists: cert_exists_msg}
        121
        122 # Print the appropriate message for this error.
        123 msg = out_msg[resp_msg] if resp_msg in out_msg else None
        124 if not msg:
        125 # We shouldn't reach here. Adding this for forward compatibility.
        126 return
        127 with fr.Message(msg, title=TITLE, lvl=frontend.ERROR,
        128 ok_button=True) as m:
        129 m.show()
        130
        131
        132def request_certificate(csr, client_id, token, base_url, enc):
        133 """Request a certificate for a registered Rok Application.
        134
        135 Send a CSR (Certificate Signing Request) for the Rok Application that
        136 corresponds to the given client ID, and that is registered with a specific
        137 Rok Registry.
        138 """
        139 api_endpoint = "v1/apps/" + client_id + "/csr"
        140 auth_header = rok_http.format_authorization_header(token)
        141 headers = {"Authorization": auth_header}
        142
        143 url = rok_http.construct_url(base_url, api_endpoint)
        144 csr_str = csr.serialize(encoding=enc)
        145
        146 client = rok_http.RokHTTPClient(url)
        147 resp = client.session.post(url, headers=headers, json={"csr": csr_str})
        148 handle_40x_status(resp)
        149 client.raise_for_status(resp, 200)
        150
        151 body = json.loads(resp.content)
        152 cert = str(body["certificate"])
        153 return cert
        154
        155
        156def store_certificate(cert, enc, csr, backend, provider_id):
        157 """Store the new signed certificate in the IndexerInfo Document."""
        158 key_str = csr.keypair.private_key.serialize(encoding=enc)
        159 key_str = protect.protect_obj(key_str)
        160
        161 cert = certificates.Certificate.load(cert, encoding=enc)
        162 cert_str = cert.serialize_certificate(encoding=enc)
        163
        164 indx = gw_backend.IndexerInfo.get(backend, indexer=provider_id)
        165 indx.update(cert=cert_str, key=key_str)
        166
        167
        168# CLI
        169######################################################
        170
        171
        172class RokRegistryAppCertCreator(RokCLI):
        173 """Create a new certificate for a registered Rok Application."""
        174
        175 DESCRIPTION = "Create a new certificate for a registered Rok Application."
        176 DEFAULT_FRONTEND = "readline"
        177 DEFAULT_LOGFILE = "gw-cert-renew.log"
        178
        179 def initialize_parser(self):
        180 parser = super(RokRegistryAppCertCreator, self).initialize_parser()
        181 questions = [ClientID(), RokRegistryAppToken()]
        182 self.questions = questions
        183 question.add_questions_args(questions, parser)
        184 return parser
        185
        186 def _ask_questions(self):
        187 for q in self.questions:
        188 self.qctx.add_question(q)
        189 q.ask()
        190
        191 def main(self):
        192 self._ask_questions()
        193
        194 client_id = self.qctx.questions[ClientID.name].get_answer()
        195 token = self.qctx.questions[RokRegistryAppToken.name].get_answer()
        196
        197 enc = encodings.PEM_NO_NEWLINES
        198
        199 backend = gw_cache.get_gw_backend()
        200 indexers = gw_backend.IndexerInfo.list(backend)
        201
        202 for indexer in indexers:
        203 if indexer.client_id == client_id:
        204 base_url = indexer.base_url
        205 provider_id = indexer.id
        206 break
        207 else:
        208 headers = ["REGISTRY URL", "CLIENT ID"]
        209 attrs = ["base_url", "client_id"]
        210 table = printutils.create_horizontal_table(headers, header=True,
        211 border=True)
        212 for indexer in indexers:
        213 table.add_row([get_field(indexer, attr) for attr in attrs])
        214 msg = (u"The provided client ID `%s' does not correspond to any"
        215 u" Rok application in this Rok cluster. Make sure you are"
        216 u" using the correct client ID. This Rok cluster is"
        217 u" currently registered to the following Rok Registries"
        218 u" with the following client IDs:\n\n%s" %
        219 (client_id, str(table)))
        220 with fr.Message(msg, title=TITLE, lvl=frontend.ERROR,
        221 ok_button=True) as m:
        222 m.show()
        223 raise ValueError("Invalid value for the Client ID")
        224
        225 # Generate CSR
        226 dn = certificates.CertificateDN.from_common_name(client_id)
        227 csr = certificates.CSR.generate(dn)
        228
        229 # Make the CSR request
        230 cert = request_certificate(csr, client_id, token, base_url, enc)
        231
        232 # Store the new certificate
        233 store_certificate(cert, enc, csr, backend, provider_id)
        234
        235 msg = (u"You have successfully renewed the certificate for the Rok"
        236 u" Application with client ID `%s' which is registered with"
        237 u" the Rok Registry with the following URL:\n\n"
        238 u"`%s'" % (client_id, base_url))
        239
        240 with fr.Message(msg, title=TITLE, lvl=frontend.SUCCESS,
        241 ok_button=True) as m:
        242 m.show()
        243
        244
        245def main():
        246 # Load django settings according to:
        247 # https://docs.djangoproject.com/en/2.1/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage # noqa: E501
        248 if not settings.configured and "DJANGO_SETTINGS_MODULE" not in os.environ:
        249 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rok_gw_prj.settings")
        250 django.setup()
        251
        252 cli = RokRegistryAppCertCreator()
        253 return cli.run()
        254
        255
        256if __name__ == "__main__":
        257 sys.exit(main())

        You can download the cert_renew.py script and store it in your /root directory as such:

        root@rok-tools:~# wget -O /root/cert_renew.py https://docs.arrikto.com/_downloads/cert_renew.py
      2. Run the cert_renew.py script:

        root@rok-tools:~# kubectl exec -it \ > -n rok \ > svc/rok \ > -- \ > python2 -c "$(cat /root/cert_renew.py)" \ > --client-id ${CLIENT_ID?} \ > --token ${APP_TOKEN?} Create a new Certificate for a registered Rok Application: You have successfully renewed the certificate for the Rok Application with client ID `wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhs' which is registered with the Rok Registry with the following URL: `https://arrikto-cluster-1.apps.example.com/registry/'
    9. Restart the sync daemon to load the new certificate:

      root@rok-tools:~# kubectl exec -it \ > -n rok \ > svc/rok \ > -- \ > rok-daemon restart throwerd.0 [ ok ] Stopping throwerd.0 (via systemctl)...done. [ ok ] Starting throwerd.0 (via systemctl)...done.
    10. Repeat the procedure for the next registered Rok cluster you want to renew the certificate for. Switch back to the management environment for Rok Registry and continue from step 5a.

Verify

  1. Go to the management environment of the Rok Registry.

  2. List all registered Rok clusters:

    1. Download the app_list.py script provided below and store it in your /root directory.

      app_list.py
      1#!/usr/bin/env python2
      2# -*- coding: utf-8 -*-
      3#
      4# This file is part of Rok.
      5#
      6# Copyright © 2022 Arrikto Inc. All Rights Reserved.
      7
      8"""Helper script to list the applications in Rok Registry."""
      9-191
      9
      10
      11import os
      12import sys
      13import pytz
      14from django.db import connection
      15from django.conf import settings
      16
      17from rok_common import printutils
      18from rok_common.crypto import encodings
      19from rok_common.crypto.certificates import Certificate
      20from rok_tasks import frontend, question
      21from rok_tasks.frontend.cli import RokCLI
      22
      23
      24fr = frontend.get_frontend()
      25
      26TITLE = "List Applications in Rok Registry"
      27
      28# Questions
      29######################################################
      30
      31
      32class UserNameQuestion(question.LineInputQuestion):
      33 """The name of the Rok Registry user."""
      34
      35 title = TITLE + ": Username"
      36 name = "question/user"
      37 msg = "What is the name of the Rok Registry user?"
      38 desc = ("The name of the Rok Registry user to obtain the applications for."
      39 " Leave empty to select all users.")
      40 envvars = "ROK_REGISTRY_USERNAME"
      41 argument_opts = {
      42 "name": ["--user"],
      43 "metavar": "USER",
      44 "help": ("List applications of user %(metavar)s. Leave empty to select"
      45 " all users.")
      46 }
      47 default = ""
      48 required = True
      49 allow_empty = True
      50 priority = question.LOW
      51
      52# Helpers
      53######################################################
      54
      55
      56def print_table(objects, headers, obj_attrs):
      57 """Print the table for the Indexer applications."""
      58 table = printutils.create_horizontal_table(headers, header=True,
      59 border=True)
      60 for obj in objects:
      61 table.add_row([obj[attr] for attr in obj_attrs])
      62 msg = "\n" + str(table)
      63 with fr.Message(msg, title=TITLE, lvl=frontend.INFO) as m:
      64 m.show()
      65
      66# CLI
      67######################################################
      68
      69
      70class ApplicationListCLI(RokCLI):
      71 """CLI tool to list the applications in Rok Registry."""
      72
      73 DESCRIPTION = "List the applications in Rok Registry."
      74
      75 # XXX: The dialog frontend uses fixed dimensions for the messagebox, so it
      76 # cannot print a wide table without wrapping. Therefore, make the readline
      77 # the default frontend for now.
      78 DEFAULT_FRONTEND = "readline"
      79 DEFAULT_LOGFILE = "indexer-app-list.log"
      80
      81 def initialize_parser(self):
      82 parser = super(ApplicationListCLI, self).initialize_parser()
      83 questions = [UserNameQuestion()]
      84 question.add_questions_args(questions, parser)
      85 return parser
      86
      87 def ask_questions(self):
      88 questions = [UserNameQuestion()]
      89 for q in questions:
      90 self.qctx.add_question(q)
      91 q.ask()
      92
      93 @classmethod
      94 def get_app_url(cls, app):
      95 """Get the URL of an Indexer app."""
      96 # FIXME: Ideally, we should obtain the application's URL from the
      97 # 'website_url' field of the underlying Fort app.
      98 #
      99 # However, the current situation for Rok clusters is that this field
      100 # points to the URL of the internal Kubernetes service. See also:
      101 # https://github.com/arrikto/rok/issues/3828
      102 #
      103 # Use the redirect URI as a temporary workaround.
      104 redirect_uris = app["redirect_uris"].split()
      105 return str(redirect_uris[0].split("/")[-4])
      106
      107 @classmethod
      108 def format_datetime(cls, datetime):
      109 """Format a datetime object into human-readable form."""
      110 if not datetime.tzinfo:
      111 datetime = datetime.replace(tzinfo=pytz.UTC)
      112 return datetime.strftime("%Y-%m-%d %H:%M:%S %Z")
      113
      114 @classmethod
      115 def print_apps(cls, indx_apps):
      116 """Print a list of Indexer apps."""
      117 # Set up attributes for printing.
      118 for app in indx_apps:
      119 app["url"] = cls.get_app_url(app)
      120 if app["certificate"]:
      121 try:
      122 cert = Certificate.load(str(app["certificate"]),
      123 encoding=encodings.PEM_NO_NEWLINES)
      124 except ValueError:
      125 msg = ("Failed to load the certificate for application"
      126 " `%s'." % app["client_id"])
      127 raise ValueError(msg)
      128 app["certificate_expires_at"] = \
      129 cls.format_datetime(cert.expiration_date)
      130 else:
      131 # No certificate exists for this app. Reserve the empty string
      132 # for this situation. Note that certificates with infinite
      133 # duration are not allowed, so probably this won't cause any
      134 # confusion.
      135 app["certificate_expires_at"] = ""
      136 # Re-format and save registration date in new attribute as Fort
      137 # attributes are read-only.
      138 app["_created_at"] = cls.format_datetime(app["created_at"])
      139
      140 # Print table.
      141 headers = ["CLIENT ID", "CLIENT URL", "USER", "CLIENT REGISTERED AT",
      142 "CERTIFICATE EXPIRES AT"]
      143 obj_attrs = ["client_id", "url", "username", "_created_at",
      144 "certificate_expires_at"]
      145 print_table(indx_apps, headers, obj_attrs)
      146
      147 def main(self):
      148 """List the applications in Rok Registry."""
      149 self.ask_questions()
      150 account_name = self.qctx.questions[UserNameQuestion.name].get_answer()
      151
      152 with connection.cursor() as cursor:
      153 # FIXME: We make the assumption here that the Indexer and Fort are
      154 # using the same database. The ideal would be to find Fort's
      155 # database from its own django settings, but currently we cannot
      156 # import them from Python2.
      157 query = ("WITH indx_apps AS ( "
      158 " SELECT * "
      159 " FROM rok_fort_api_application "
      160 " NATURAL JOIN rok_indexer_api_applicationmodel) "
      161 "SELECT indx_apps.*, "
      162 " rok_fort_api_account.display_name as username "
      163 "FROM indx_apps "
      164 "INNER JOIN rok_fort_api_account "
      165 "ON indx_apps.user_id = rok_fort_api_account.uuid")
      166 cursor.execute(query)
      167 columns = [col[0] for col in cursor.description]
      168 indx_apps = [dict(zip(columns, row)) for row in cursor.fetchall()]
      169
      170 if account_name:
      171 # Filter out the Indexer applications that do not belong to the
      172 # provided user.
      173 with connection.cursor() as cursor:
      174 cursor.execute("SELECT uuid FROM rok_fort_api_account WHERE"
      175 " display_name = %s", [account_name])
      176 result = cursor.fetchall()
      177 if not result:
      178 raise ValueError("Invalid user `%s'." % account_name)
      179 account_uuid = result[0][0]
      180 indx_apps = filter(lambda app: app["user_id"] == account_uuid,
      181 indx_apps)
      182
      183 # Print the Indexer apps.
      184 self.print_apps(indx_apps)
      185
      186
      187def main():
      188 # Load django settings according to:
      189 # https://docs.djangoproject.com/en/2.1/topics/settings/#either-configure-or-django-settings-module-is-required # noqa: E501
      190 if not settings.configured and "DJANGO_SETTINGS_MODULE" not in os.environ:
      191 os.environ.setdefault("DJANGO_SETTINGS_MODULE",
      192 "rok_indexer_prj.settings")
      193
      194 cli = ApplicationListCLI()
      195 return cli.run()
      196
      197
      198if __name__ == "__main__":
      199 sys.exit(main())

      You can download the app_list.py script and store it in your /root directory as such:

      root@rok-tools:~# wget -O /root/app_list.py https://docs.arrikto.com/_downloads/app_list.py
    2. Run the app_list.py script:

      root@rok-tools:~# kubectl exec -ti \ > svc/rok-registry \ > -n rok-registry -- \ > python2 -c "$(cat /root/app_list.py)" List Applications in Rok Registry: -------------------------------------------------------------------------------------------------------------------------------------------- CLIENT ID CLIENT URL USER CLIENT REGISTERED AT CERTIFICATE EXPIRES AT -------------------------------------------------------------------------------------------------------------------------------------------- wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhs arrikto-cluster-1.apps.example.com user 2022-03-10 16:48:33 UTC 2024-01-15 10:35:37 UTC U5sWM4yGT09hW95Se5MS6zP13W07TQDc1n8C2lhO arrikto-cluster-2.apps.example.com user 2022-08-17 16:50:32 UTC 2024-01-15 10:36:46 UTC 84IDEyNyePoRXAUu9zIJGRSX6hXfgKc2AMB0bR67 arrikto-cluster-3.apps.example.com user 2022-10-25 16:31:14 UTC 2024-01-15 10:37:58 UTC --------------------------------------------------------------------------------------------------------------------------------------------
  3. Inspect the output of the previous step. Make sure that all the certificates you have renewed expire in one year.

Summary

You have successfully renewed the certificates for your Rok clusters.

What’s Next

Check out the rest of the operations you can perform on your Rok Registry deployment.