Mohammed Naser | f3f59a7 | 2023-01-15 21:02:04 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | {{/* |
| 4 | Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | you may not use this file except in compliance with the License. |
| 6 | You may obtain a copy of the License at |
| 7 | |
| 8 | http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | |
| 10 | Unless required by applicable law or agreed to in writing, software |
| 11 | distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | See the License for the specific language governing permissions and |
| 14 | limitations under the License. |
| 15 | */}} |
| 16 | |
| 17 | import argparse |
| 18 | import base64 |
| 19 | import errno |
| 20 | import grp |
| 21 | import logging |
| 22 | import os |
| 23 | import pwd |
| 24 | import re |
Mohammed Naser | f3f59a7 | 2023-01-15 21:02:04 -0500 | [diff] [blame] | 25 | import subprocess #nosec |
| 26 | import sys |
| 27 | import time |
| 28 | |
| 29 | import requests |
| 30 | |
| 31 | FERNET_DIR = os.environ['KEYSTONE_KEYS_REPOSITORY'] |
| 32 | KEYSTONE_USER = os.environ['KEYSTONE_USER'] |
| 33 | KEYSTONE_GROUP = os.environ['KEYSTONE_GROUP'] |
| 34 | NAMESPACE = os.environ['KUBERNETES_NAMESPACE'] |
| 35 | |
| 36 | # k8s connection data |
| 37 | KUBE_HOST = None |
| 38 | KUBE_CERT = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' |
| 39 | KUBE_TOKEN = None |
| 40 | |
| 41 | LOG_DATEFMT = "%Y-%m-%d %H:%M:%S" |
| 42 | LOG_FORMAT = "%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s" |
| 43 | logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATEFMT) |
| 44 | LOG = logging.getLogger(__name__) |
| 45 | LOG.setLevel(logging.INFO) |
| 46 | |
| 47 | |
| 48 | def read_kube_config(): |
| 49 | global KUBE_HOST, KUBE_TOKEN |
| 50 | KUBE_HOST = "https://%s:%s" % ('kubernetes.default', |
| 51 | os.environ['KUBERNETES_SERVICE_PORT']) |
| 52 | with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r') as f: |
| 53 | KUBE_TOKEN = f.read() |
| 54 | |
| 55 | |
| 56 | def get_secret_definition(name): |
| 57 | url = '%s/api/v1/namespaces/%s/secrets/%s' % (KUBE_HOST, NAMESPACE, name) |
| 58 | resp = requests.get(url, |
| 59 | headers={'Authorization': 'Bearer %s' % KUBE_TOKEN}, |
| 60 | verify=KUBE_CERT) |
| 61 | if resp.status_code != 200: |
| 62 | LOG.error('Cannot get secret %s.', name) |
| 63 | LOG.error(resp.text) |
| 64 | return None |
| 65 | return resp.json() |
| 66 | |
| 67 | |
| 68 | def update_secret(name, secret): |
| 69 | url = '%s/api/v1/namespaces/%s/secrets/%s' % (KUBE_HOST, NAMESPACE, name) |
| 70 | resp = requests.put(url, |
| 71 | json=secret, |
| 72 | headers={'Authorization': 'Bearer %s' % KUBE_TOKEN}, |
| 73 | verify=KUBE_CERT) |
| 74 | if resp.status_code != 200: |
| 75 | LOG.error('Cannot update secret %s.', name) |
| 76 | LOG.error(resp.text) |
| 77 | return False |
| 78 | return True |
| 79 | |
| 80 | |
| 81 | def read_from_files(): |
| 82 | keys = [name for name in os.listdir(FERNET_DIR) if os.path.isfile(FERNET_DIR + name) |
| 83 | and re.match("^\d+$", name)] |
| 84 | data = {} |
| 85 | for key in keys: |
| 86 | with open(FERNET_DIR + key, 'r') as f: |
| 87 | data[key] = f.read() |
| 88 | if len(list(keys)): |
| 89 | LOG.debug("Keys read from files: %s", keys) |
| 90 | else: |
Oleksandr Kozachenko | a10d785 | 2023-02-02 22:01:16 +0100 | [diff] [blame] | 91 | LOG.warning("No keys were read from files.") |
Mohammed Naser | f3f59a7 | 2023-01-15 21:02:04 -0500 | [diff] [blame] | 92 | return data |
| 93 | |
| 94 | |
| 95 | def get_keys_data(): |
| 96 | keys = read_from_files() |
| 97 | return dict([(key, base64.b64encode(value.encode()).decode()) |
Oleksandr Kozachenko | a10d785 | 2023-02-02 22:01:16 +0100 | [diff] [blame] | 98 | for (key, value) in keys.items()]) |
Mohammed Naser | f3f59a7 | 2023-01-15 21:02:04 -0500 | [diff] [blame] | 99 | |
| 100 | |
| 101 | def write_to_files(data): |
| 102 | if not os.path.exists(os.path.dirname(FERNET_DIR)): |
| 103 | try: |
| 104 | os.makedirs(os.path.dirname(FERNET_DIR)) |
| 105 | except OSError as exc: # Guard against race condition |
| 106 | if exc.errno != errno.EEXIST: |
| 107 | raise |
| 108 | uid = pwd.getpwnam(KEYSTONE_USER).pw_uid |
| 109 | gid = grp.getgrnam(KEYSTONE_GROUP).gr_gid |
| 110 | os.chown(FERNET_DIR, uid, gid) |
| 111 | |
Oleksandr Kozachenko | a10d785 | 2023-02-02 22:01:16 +0100 | [diff] [blame] | 112 | for (key, value) in data.items(): |
Mohammed Naser | f3f59a7 | 2023-01-15 21:02:04 -0500 | [diff] [blame] | 113 | with open(FERNET_DIR + key, 'w') as f: |
| 114 | decoded_value = base64.b64decode(value).decode() |
| 115 | f.write(decoded_value) |
| 116 | LOG.debug("Key %s: %s", key, decoded_value) |
| 117 | LOG.info("%s keys were written", len(data)) |
| 118 | |
| 119 | |
| 120 | def execute_command(cmd): |
| 121 | LOG.info("Executing 'keystone-manage %s --keystone-user=%s " |
| 122 | "--keystone-group=%s' command.", |
| 123 | cmd, KEYSTONE_USER, KEYSTONE_GROUP) |
| 124 | subprocess.call(['keystone-manage', cmd, #nosec |
| 125 | '--keystone-user=%s' % KEYSTONE_USER, |
| 126 | '--keystone-group=%s' % KEYSTONE_GROUP]) |
| 127 | |
| 128 | def main(): |
| 129 | parser = argparse.ArgumentParser() |
| 130 | parser.add_argument('command', choices=['fernet_setup', 'fernet_rotate', |
| 131 | 'credential_setup', |
| 132 | 'credential_rotate']) |
| 133 | args = parser.parse_args() |
| 134 | |
| 135 | is_credential = args.command.startswith('credential') |
| 136 | |
| 137 | SECRET_NAME = ('keystone-credential-keys' if is_credential else |
| 138 | 'keystone-fernet-keys') |
| 139 | |
| 140 | read_kube_config() |
| 141 | secret = get_secret_definition(SECRET_NAME) |
| 142 | if not secret: |
| 143 | LOG.error("Secret '%s' does not exist.", SECRET_NAME) |
| 144 | sys.exit(1) |
| 145 | |
| 146 | if args.command in ('fernet_rotate', 'credential_rotate'): |
| 147 | LOG.info("Copying existing %s keys from secret '%s' to %s.", |
| 148 | 'credential' if is_credential else 'fernet', SECRET_NAME, |
| 149 | FERNET_DIR) |
| 150 | write_to_files(secret['data']) |
| 151 | |
| 152 | if args.command in ('credential_setup', 'fernet_setup'): |
| 153 | if secret.get('data', False): |
| 154 | LOG.info('Keys already exist, skipping setup...') |
| 155 | sys.exit(0) |
| 156 | |
| 157 | execute_command(args.command) |
| 158 | |
| 159 | LOG.info("Updating data for '%s' secret.", SECRET_NAME) |
| 160 | updated_keys = get_keys_data() |
| 161 | secret['data'] = updated_keys |
| 162 | if not update_secret(SECRET_NAME, secret): |
| 163 | sys.exit(1) |
| 164 | LOG.info("%s fernet keys have been placed to secret '%s'", |
| 165 | len(updated_keys), SECRET_NAME) |
| 166 | LOG.debug("Placed keys: %s", updated_keys) |
| 167 | LOG.info("%s keys %s has been completed", |
| 168 | "Credential" if is_credential else 'Fernet', |
| 169 | "rotation" if args.command.endswith('_rotate') else "generation") |
| 170 | |
| 171 | if args.command == 'credential_rotate': |
| 172 | # `credential_rotate` needs doing `credential_migrate` as well once all |
| 173 | # of the nodes have the new keys. So we'll sleep configurable amount of |
| 174 | # time to make sure k8s reloads the secrets in all pods and then |
| 175 | # execute `credential_migrate`. |
| 176 | |
| 177 | migrate_wait = int(os.getenv('KEYSTONE_CREDENTIAL_MIGRATE_WAIT', "60")) |
| 178 | LOG.info("Waiting %d seconds to execute `credential_migrate`.", |
| 179 | migrate_wait) |
| 180 | time.sleep(migrate_wait) |
| 181 | |
| 182 | execute_command('credential_migrate') |
| 183 | |
| 184 | if __name__ == "__main__": |
| 185 | main() |