blob: a93cab876798bf0538d76fd11073c0bcb04cea24 [file] [log] [blame]
Mohammed Naserf3f59a72023-01-15 21:02:04 -05001#!/usr/bin/env python
2
3{{/*
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/}}
16
17import argparse
18import base64
19import errno
20import grp
21import logging
22import os
23import pwd
24import re
Mohammed Naserf3f59a72023-01-15 21:02:04 -050025import subprocess #nosec
26import sys
27import time
28
29import requests
30
31FERNET_DIR = os.environ['KEYSTONE_KEYS_REPOSITORY']
32KEYSTONE_USER = os.environ['KEYSTONE_USER']
33KEYSTONE_GROUP = os.environ['KEYSTONE_GROUP']
34NAMESPACE = os.environ['KUBERNETES_NAMESPACE']
35
36# k8s connection data
37KUBE_HOST = None
38KUBE_CERT = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
39KUBE_TOKEN = None
40
41LOG_DATEFMT = "%Y-%m-%d %H:%M:%S"
42LOG_FORMAT = "%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s"
43logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATEFMT)
44LOG = logging.getLogger(__name__)
45LOG.setLevel(logging.INFO)
46
47
48def 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
56def 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
68def 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
81def 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 Kozachenkoa10d7852023-02-02 22:01:16 +010091 LOG.warning("No keys were read from files.")
Mohammed Naserf3f59a72023-01-15 21:02:04 -050092 return data
93
94
95def get_keys_data():
96 keys = read_from_files()
97 return dict([(key, base64.b64encode(value.encode()).decode())
Oleksandr Kozachenkoa10d7852023-02-02 22:01:16 +010098 for (key, value) in keys.items()])
Mohammed Naserf3f59a72023-01-15 21:02:04 -050099
100
101def 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 Kozachenkoa10d7852023-02-02 22:01:16 +0100112 for (key, value) in data.items():
Mohammed Naserf3f59a72023-01-15 21:02:04 -0500113 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
120def 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
128def 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
184if __name__ == "__main__":
185 main()