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.
Overview
What You’ll Need¶
- A configured management environment that has access to your Rok Registry cluster.
- A working Rok and Rok Registry deployment.
Procedure¶
Go to the management environment of the Rok Registry.
Note
Keep an open shell for that Rok Registry to use it throughout the guide.
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 42mDownload the Python scripts you will use later to manage your certificates:
Note
In the following scripts, we refer to Rok clusters as Rok applications.
Download the
app_list.py
script provided below and store it in your/root
directory.app_list.py1 #!/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 11 import os 12 import sys 13 import pytz 14 from django.db import connection 15 from django.conf import settings 16 17 from rok_common import printutils 18 from rok_common.crypto import encodings 19 from rok_common.crypto.certificates import Certificate 20 from rok_tasks import frontend, question 21 from rok_tasks.frontend.cli import RokCLI 22 23 24 fr = frontend.get_frontend() 25 26 TITLE = "List Applications in Rok Registry" 27 28 # Questions 29 ###################################################### 30 31 32 class 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 56 def 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 70 class 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 187 def 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 198 if __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.pyDownload the
cert_delete.py
script provided below and store it in your/root
directory.cert_delete.py1 #!/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 11 import os 12 import sys 13 import django 14 15 from django.conf import settings 16 from django.db import transaction 17 from django.utils.timezone import now 18 19 from rok_tasks import frontend, question 20 from rok_tasks.frontend.cli import RokCLI 21 22 23 fr = frontend.get_frontend() 24 25 TITLE = "Delete Certificate of Rok Registry Application" 26 27 # Questions 28 ###################################################### 29 30 31 class 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 48 class 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 76 class 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 129 def 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 140 if __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
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 --------------------------------------------------------------------------------------------------------------------------------------------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.
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=wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhsDelete 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.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 gIZtj9N8j5EK5z8dqsYcEDqjszCeJdGo to the management environment of your Rok cluster. Use the client URL from step 4 to determine the Rok cluster.
Validate that you are in the right management environment:
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.comMake sure that the subdomain from the previous step matches the URL from step 4.
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=wfp2r3KHnmWGyhcofdV0OoAFgDF7TM1h89cONyhsroot@rok-tools:~# export APP_TOKEN=gIZtj9N8j5EK5z8dqsYcEDqjszCeJdValidate 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 TRUETroubleshooting
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:
Switch back to the management environment for Rok Registry.
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.Continue from step 5a to renew the certificates for the remaining of your selected Rok clusters.
Create a new certificate for the Rok cluster:
Download the
cert_renew.py
script provided below and store it in your/root
directory.cert_renew.py1 #!/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 11 This Rok Application should be registered with a Rok Registry. 12 """ 13 14 import os 15 import sys 16 import json 17 import django 18 from django.conf import settings 19 20 from rok_gw import backend as gw_backend 21 from rok_gw_api import cache as gw_cache 22 from rok_common.crypto import certificates, encodings 23 from rok_common import protect, printutils, http as rok_http 24 from rok_django_lib.management import get_field 25 26 from rok_tasks import frontend, question 27 from rok_tasks.frontend.cli import RokCLI 28 29 30 fr = frontend.get_frontend() 31 32 TITLE = "Create a new Certificate for a registered Rok Application" 33 DESCRIPTION = "Create a new Certificate for a registered Rok Application" 34 35 # Questions 36 ###################################################### 37 38 39 class 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 56 class 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 78 def 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 88 def 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 132 def 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 156 def 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 172 class 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 245 def 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 256 if __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.pyRun 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/'
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.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¶
Go to the management environment of the Rok Registry.
List all registered Rok clusters:
Download the
app_list.py
script provided below and store it in your/root
directory.app_list.py1 #!/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 11 import os 12 import sys 13 import pytz 14 from django.db import connection 15 from django.conf import settings 16 17 from rok_common import printutils 18 from rok_common.crypto import encodings 19 from rok_common.crypto.certificates import Certificate 20 from rok_tasks import frontend, question 21 from rok_tasks.frontend.cli import RokCLI 22 23 24 fr = frontend.get_frontend() 25 26 TITLE = "List Applications in Rok Registry" 27 28 # Questions 29 ###################################################### 30 31 32 class 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 56 def 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 70 class 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 187 def 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 198 if __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.pyRun 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 --------------------------------------------------------------------------------------------------------------------------------------------
Inspect the output of the previous step. Make sure that all the certificates you have renewed expire in one year.
What’s Next¶
Check out the rest of the operations you can perform on your Rok Registry deployment.