Docker registry certificate management by cert-manager

Cert-mon changes to monitor 'system-docker-local-certificate' k8s secret
and install StarlingX docker_registry certificate for
registry.local:9001.

Changes include:
- New thread to watch registry certificate changes
- Refactored the code to reduce code duplication
- Call sysinv api for 'certificate_install'

Design testing completed:
- When k8s secret is added/modified (initiated by cert-manager),
  certificate installation is completed
- sysinv api 'certificate_install' installs & confirmed via openssl
s_client -connect registry.local:9001
- When certificate is renewed, keys get regenerated (no changes
  needed. Confirmed that existing infrastructure takes care of it)

Story: 2007361
Task: 41717
Change-Id: Iffa68486764287a1b82a183ab9801a53c1e4885b
Signed-off-by: Isac Souza <IsacSacchi.Souza@windriver.com>
This commit is contained in:
Isac Souza 2021-01-28 12:44:09 -03:00
parent 53e3797fca
commit 983add3417
7 changed files with 141 additions and 69 deletions

View File

@ -316,16 +316,16 @@ class CertificateController(rest.RestController):
force = True
else:
force = False
# if PLATFORM_CERT_SECRET_NAME secret is present in k8s, we
# if RESTAPI_CERT_SECRET_NAME secret is present in k8s, we
# assume that SSL cert is managed by cert-manager/cert-mon
managed_by_cm = self._kube_op.kube_get_secret(
constants.PLATFORM_CERT_SECRET_NAME,
constants.RESTAPI_CERT_SECRET_NAME,
constants.CERT_NAMESPACE_PLATFORM_CERTS)
if force is False and managed_by_cm is not None:
msg = "Certificate is currently being managed by cert-manager. \n" \
"To manage certificate with this command, first delete " \
"the %s Certificate and Secret." % constants.PLATFORM_CERT_SECRET_NAME
"the %s Certificate and Secret." % constants.RESTAPI_CERT_SECRET_NAME
LOG.info(msg)
return dict(success="", error=msg)

View File

@ -451,12 +451,12 @@ class SystemController(rest.RestController):
# while 'ssl' cert is managed by cert-manager, return error
# (Otherwise, cert-mon will turn https back on during cert-renewal process)
managed_by_cm = self._kube_op.kube_get_secret(
constants.PLATFORM_CERT_SECRET_NAME,
constants.RESTAPI_CERT_SECRET_NAME,
constants.CERT_NAMESPACE_PLATFORM_CERTS)
if https_enabled == 'false' and managed_by_cm is not None:
msg = "Certificate is currently being managed by cert-manager. " \
"Remove %s Certificate and Secret before disabling https." % \
constants.PLATFORM_CERT_SECRET_NAME
constants.RESTAPI_CERT_SECRET_NAME
raise wsme.exc.ClientSideError(_(msg))
if https_enabled != rpc_isystem['capabilities']['https_enabled']:

View File

@ -54,11 +54,11 @@ CONF.register_opts(cert_mon_opts, 'certmon')
class CertificateMonManager(periodic_task.PeriodicTasks):
def __init__(self):
super(CertificateMonManager, self).__init__(CONF)
self.dc_mon_thread = None
self.platcert_mon_thread = None
self.mon_threads = []
self.audit_thread = None
self.dc_monitor = None
self.platcert_monitor = None
self.restapicert_monitor = None
self.registrycert_monitor = None
self.reattempt_tasks = []
self.subclouds_to_audit = []
@ -190,17 +190,22 @@ class CertificateMonManager(periodic_task.PeriodicTasks):
self.dc_monitor.initialize(
audit_subcloud=lambda subcloud_name: self.requeue_audit(subcloud_name))
def init_platformcert_monitor(self):
self.platcert_monitor = watcher.PlatCert_CertWatcher()
self.platcert_monitor.initialize()
def init_restapicert_monitor(self):
self.restapicert_monitor = watcher.RestApiCert_CertWatcher()
self.restapicert_monitor.initialize()
def init_registrycert_monitor(self):
self.registrycert_monitor = watcher.RegistryCert_CertWatcher()
self.registrycert_monitor.initialize()
def start_monitor(self):
utils.init_keystone_auth_opts()
dc_role = utils.get_dc_role()
while True:
try:
# init platformcert monitor
self.init_platformcert_monitor()
# init platform cert monitors
self.init_restapicert_monitor()
self.init_registrycert_monitor()
# init dc monitor only if running in DC role
if (dc_role == constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER or
@ -213,20 +218,17 @@ class CertificateMonManager(periodic_task.PeriodicTasks):
break
# spawn threads (DC thread spawned only if running in DC role)
self.platcert_mon_thread = greenthread.spawn(self.platcert_monitor_cert)
self.mon_threads.append(greenthread.spawn(self.monitor_cert, self.restapicert_monitor))
self.mon_threads.append(greenthread.spawn(self.monitor_cert, self.registrycert_monitor))
if (dc_role == constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER or
dc_role == constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
self.dc_mon_thread = greenthread.spawn(self.dc_monitor_cert)
self.mon_threads.append(greenthread.spawn(self.monitor_cert, self.dc_monitor))
def stop_monitor(self):
if self.dc_mon_thread:
self.dc_mon_thread.kill()
self.dc_mon_thread.wait()
if self.platcert_mon_thread:
self.platcert_mon_thread.kill()
self.platcert_mon_thread.wait()
for mon_thread in self.mon_threads:
mon_thread.kill()
mon_thread.wait()
def stop_audit(self):
if self.audit_thread:
@ -243,26 +245,11 @@ class CertificateMonManager(periodic_task.PeriodicTasks):
except Exception as e:
LOG.exception(e)
def dc_monitor_cert(self):
def monitor_cert(self, monitor):
while True:
# never exit until exit signal received
try:
self.dc_monitor.start_watch(
on_success=lambda task_id: self._purge_reattempt_task(task_id),
on_error=lambda task: self._add_reattempt_task(task),
)
except greenlet.GreenletExit:
break
except Exception as e:
# A bug somewhere?
# It shouldn't fall to here, but log and restart if it did
LOG.exception(e)
def platcert_monitor_cert(self):
while True:
# never exit until exit signal received
try:
self.platcert_monitor.start_watch(
monitor.start_watch(
on_success=lambda task_id: self._purge_reattempt_task(task_id),
on_error=lambda task: self._add_reattempt_task(task),
)

View File

@ -576,8 +576,8 @@ def rest_api_upload(token, filepath, url, data=None):
return upload_request_with_data(token, url, body=file_to_upload, data=data)
def update_platformcert_pemfile(tls_crt, tls_key):
LOG.info('Updating platformcert temporary pemfile')
def update_pemfile(tls_crt, tls_key):
LOG.info('Updating temporary pemfile')
try:
fd, tmppath = tempfile.mkstemp(suffix='.pem')
with open(tmppath, 'w+') as f:
@ -593,13 +593,20 @@ def update_platformcert_pemfile(tls_crt, tls_key):
return tmppath
def update_platform_cert(token, pem_file_path):
LOG.info('Updating platform certificate. pem_file_path=%s' % pem_file_path)
def update_platform_cert(token, cert_type, pem_file_path, force=False):
"""Update a platform certificate using the sysinv API
:param token: the token to access the sysinv API
:param cert_type: the type of the certificate that is being updated
:param pem_file_path: path to the certificate file in PEM format
:param force: whether to bypass semantic checks and force the update,
defaults to False
"""
LOG.info('Updating %s certificate. pem_file_path=%s' % (cert_type, pem_file_path))
sysinv_url = token.get_service_internal_url(constants.SERVICE_TYPE_PLATFORM, constants.SYSINV_USERNAME)
api_cmd = sysinv_url + '/certificate/certificate_install'
data = {'mode': 'ssl',
'force': 'true'}
data = {'mode': cert_type,
'force': str(force).lower()}
response = rest_api_upload(token, pem_file_path, api_cmd, data)
error = response.get('error')

View File

@ -256,18 +256,32 @@ class DC_CertWatcher(CertWatcher):
self.register_listener(RootCARenew(self.context))
class PlatCert_CertWatcher(CertWatcher):
class RestApiCert_CertWatcher(CertWatcher):
def __init__(self):
super(PlatCert_CertWatcher, self).__init__()
super(RestApiCert_CertWatcher, self).__init__()
def initialize(self):
self.context.initialize()
platcert_ns = constants.CERT_NAMESPACE_PLATFORM_CERTS
LOG.info('setting ns : %s & registering listener' % platcert_ns)
LOG.info('setting ns for restapi cert : %s & registering listener' % platcert_ns)
self.namespace = platcert_ns
self.context.kubernete_namespace = platcert_ns
self.register_listener(PlatformCertRenew(self.context))
self.register_listener(RestApiCertRenew(self.context))
class RegistryCert_CertWatcher(CertWatcher):
def __init__(self):
super(RegistryCert_CertWatcher, self).__init__()
def initialize(self):
self.context.initialize()
platcert_ns = constants.CERT_NAMESPACE_PLATFORM_CERTS
LOG.info('setting ns for registry cert : %s & registering listener' % platcert_ns)
self.namespace = platcert_ns
self.context.kubernete_namespace = platcert_ns
self.register_listener(RegistryCertRenew(self.context))
class CertificateRenew(CertWatcherListener):
@ -496,23 +510,58 @@ class RootCARenew(CertificateRenew):
class PlatformCertRenew(CertificateRenew):
def __init__(self, context):
"""Handles a renew event for a certificate that must be installed as a platform cert.
"""
def __init__(self, context, secret_name):
super(PlatformCertRenew, self).__init__(context)
self.secret_name = constants.PLATFORM_CERT_SECRET_NAME
LOG.info('PlatformCertRenew init with secretname: %s' % self.secret_name)
self.secret_name = secret_name
LOG.info('%s init with secretname: %s' % (self.__class__.__name__, self.secret_name))
def check_filter(self, event_data):
LOG.info('%s: Received event_data %s' % (self.secret_name, event_data))
if self.secret_name == event_data.secret_name:
return self.certificate_is_ready(event_data)
else:
return False
def update_platform_certificate(self, event_data, cert_type, force=False):
"""Update a platform certificate
Save the certificate and key from the secret into a PEM file and send it to the
platform to be installed. If force=True, the platform semantic checks will be
skipped.
:param event_data: the event_data that triggered this renew
:param cert_type: the type of the certificate that is being updated
:param force: whether to bypass semantic checks and force the update,
defaults to False
"""
pem_file_path = utils.update_pemfile(event_data.tls_crt, event_data.tls_key)
token = self.context.get_token()
utils.update_platform_cert(token, cert_type, pem_file_path, force)
class RestApiCertRenew(PlatformCertRenew):
def __init__(self, context):
super(RestApiCertRenew, self).__init__(context, constants.RESTAPI_CERT_SECRET_NAME)
def update_certificate(self, event_data):
LOG.info('PlatformCertRenew: Secret changes detected. Initiating certificate update')
LOG.info('RestApiCertRenew: Secret changes detected. Initiating certificate update')
token = self.context.get_token()
system_uuid = utils.get_isystems_uuid(token)
ret = utils.enable_https(token, system_uuid)
pem_file_path = utils.update_platformcert_pemfile(event_data.tls_crt,
event_data.tls_key)
if ret is True:
utils.update_platform_cert(token, pem_file_path)
self.update_platform_certificate(event_data, constants.CERT_MODE_SSL, force=True)
class RegistryCertRenew(PlatformCertRenew):
def __init__(self, context):
super(RegistryCertRenew, self).__init__(context, constants.REGISTRY_CERT_SECRET_NAME)
def update_certificate(self, event_data):
LOG.info('RegistryCertRenew: Secret changes detected. Initiating certificate update')
self.update_platform_certificate(event_data, constants.CERT_MODE_DOCKER_REGISTRY)

View File

@ -1735,5 +1735,6 @@ DC_ROOT_CA_CONFIG_PATH = \
ADMIN_EP_CERT_FORMAT = '{tls_key}'
# Platform certificates
PLATFORM_CERT_SECRET_NAME = "system-restapi-gui-certificate"
RESTAPI_CERT_SECRET_NAME = "system-restapi-gui-certificate"
REGISTRY_CERT_SECRET_NAME = "system-registry-local-certificate"
CERT_NAMESPACE_PLATFORM_CERTS = 'deployment'

View File

@ -42,19 +42,18 @@ class CertMonTestCase(base.DbTestCase):
def tearDown(self):
super(CertMonTestCase, self).tearDown()
def test_platformcert_secret_and_ns_check(self):
def test_platform_certs_secret_and_ns_check(self):
self.assertEqual("system-restapi-gui-certificate",
constants.PLATFORM_CERT_SECRET_NAME)
constants.RESTAPI_CERT_SECRET_NAME)
self.assertEqual("system-registry-local-certificate",
constants.REGISTRY_CERT_SECRET_NAME)
self.assertEqual("deployment",
constants.CERT_NAMESPACE_PLATFORM_CERTS)
def test_update_pemfile(self):
reference_file = os.path.join(os.path.dirname(__file__),
"data", "cert-with-key.pem")
cert_filename = os.path.join(os.path.dirname(__file__),
"data", "cert.pem")
key_filename = os.path.join(os.path.dirname(__file__),
"data", "key.pem")
reference_file = self.get_data_file_path("cert-with-key.pem")
cert_filename = self.get_data_file_path("cert.pem")
key_filename = self.get_data_file_path("key.pem")
with open(cert_filename, 'r') as cfile:
tls_cert = cfile.read()
@ -62,14 +61,14 @@ class CertMonTestCase(base.DbTestCase):
with open(key_filename, 'r') as kfile:
tls_key = kfile.read()
generated_file = cert_mon_utils.update_platformcert_pemfile(tls_cert, tls_key)
generated_file = cert_mon_utils.update_pemfile(tls_cert, tls_key)
assert os.path.exists(generated_file)
assert filecmp.cmp(generated_file, reference_file, shallow=False)
os.remove(generated_file)
def get_keystone_token(self):
token_file = os.path.join(os.path.dirname(__file__), "data", "keystone-token")
token_file = self.get_data_file_path("keystone-token")
with open(token_file, 'r') as tfile:
token_json = json.load(tfile)
@ -78,7 +77,7 @@ class CertMonTestCase(base.DbTestCase):
return Token(token_json, token_id, region_name)
def test_get_isystems_uuid(self):
isystems_file = os.path.join(os.path.dirname(__file__), "data", "isystems")
isystems_file = self.get_data_file_path("isystems")
with open(isystems_file, 'r') as ifile:
self.rest_api_request_result = json.load(ifile)
@ -87,7 +86,7 @@ class CertMonTestCase(base.DbTestCase):
assert ret == 'fdc60cf3-3330-4438-859d-b0da19e9663d'
def test_enable_https(self):
isystems_file = os.path.join(os.path.dirname(__file__), "data", "isystems")
isystems_file = self.get_data_file_path("isystems")
with open(isystems_file, 'r') as ifile:
isystems_json = json.load(ifile)
@ -96,3 +95,32 @@ class CertMonTestCase(base.DbTestCase):
token = self.keystone_token
ret = cert_mon_utils.enable_https(token, 'fdc60cf3-3330-4438-859d-b0da19e9663d')
assert ret is True
def test_update_platform_cert_force_true(self):
self.update_platform_cert(True)
def test_update_platform_cert_force_false(self):
self.update_platform_cert(False)
def update_platform_cert(self, force):
token = self.keystone_token
cert_type = constants.CERT_MODE_DOCKER_REGISTRY
pem_file_path = self.get_data_file_path('cert-with-key.pem')
patcher = mock.patch('sysinv.cert_mon.utils.rest_api_upload')
mocked_rest_api_upload = patcher.start()
self.addCleanup(patcher.stop)
mocked_rest_api_upload.return_value = {'error': ''}
with mock.patch('sysinv.cert_mon.utils.os') as mocked_os:
cert_mon_utils.update_platform_cert(token, cert_type, pem_file_path, force)
mocked_os.remove.assert_called_once_with(pem_file_path)
mocked_rest_api_upload.assert_called_once_with(token, pem_file_path, mock.ANY, mock.ANY)
actual_data = mocked_rest_api_upload.call_args[0][3]
self.assertEqual(actual_data['force'], str(force).lower())
def get_data_file_path(self, file_name):
return os.path.join(os.path.dirname(__file__), "data", file_name)