Verify docker registry certificate with system trusted CAs

The issue was caused by sysinv verifying the registry certificate with
only itself. This works with the self-signed certificate installed by
the system but not if the user installs a new certificate.

This commit now checks the registry certificate against system trusted
CAs in addition to against itself. This ensures system registry
commands work with both the self-signed certificates and certificates
installed by the user that are signed by an actual CA.

Exception handling has also been improved so that sysinv can detect an
exception thrown while communicating with the registry. This is now
logged and returned to the user as opposed to hanging the sysinv
registry commands.

Closes-Bug: 1863624

Change-Id: I1170e296d5037716f2f800eede32e15861d038ce
Signed-off-by: Jerry Sun <jerry.sun@windriver.com>
This commit is contained in:
Jerry Sun 2020-05-25 13:45:55 -04:00
parent c1566745d7
commit 1cc5d0dbb3
4 changed files with 120 additions and 25 deletions

View File

@ -15,6 +15,7 @@ from sysinv._i18n import _
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.common import utils as cutils
from sysinv.openstack.common.rpc import common as rpc_common
LOG = log.getLogger(__name__)
@ -70,13 +71,20 @@ class RegistryImageController(rest.RestController):
@wsme_pecan.wsexpose(RegistryImageCollection, wtypes.text)
def get_all(self, image_name=None):
try:
# no image_name provided, list images
if image_name is None:
images = pecan.request.rpcapi.docker_registry_image_list(
pecan.request.context)
# image_name provided, list tags of provided image
else:
images = pecan.request.rpcapi.docker_registry_image_tags(
pecan.request.context, image_name)
# DockerRegistrySSLException and DockerRegistryAPIException
# come in as RemoteError from the RPC handler
except rpc_common.RemoteError as e:
raise wsme.exc.ClientSideError(_(e.value))
# no image_name provided, list images
if image_name is None:
images = pecan.request.rpcapi.docker_registry_image_list(pecan.request.context)
# image_name provided, list tags of provided image
else:
images = pecan.request.rpcapi.docker_registry_image_tags(pecan.request.context, image_name)
return RegistryImageCollection.convert_with_links(images)
@cutils.synchronized(LOCK_NAME)
@ -88,13 +96,26 @@ class RegistryImageController(rest.RestController):
"""
if len(image_name_and_tag.split(":")) != 2:
raise wsme.exc.ClientSideError(_("Image name and tag must be of form name:tag"))
raise wsme.exc.ClientSideError(_("Image name and tag must be of "
"form name:tag"))
return pecan.request.rpcapi.docker_registry_image_delete(pecan.request.context, image_name_and_tag)
try:
return pecan.request.rpcapi.docker_registry_image_delete(
pecan.request.context, image_name_and_tag)
# DockerRegistrySSLException and DockerRegistryAPIException
# come in as RemoteError from the RPC handler
except rpc_common.RemoteError as e:
raise wsme.exc.ClientSideError(_(e.value))
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, wtypes.text)
def post(self, garbage_collect=None):
"""Run the registry garbage collector"""
if garbage_collect is not None:
pecan.request.rpcapi.docker_registry_garbage_collect(pecan.request.context)
try:
pecan.request.rpcapi.docker_registry_garbage_collect(
pecan.request.context)
# DockerRegistrySSLException and DockerRegistryAPIException
# come in as RemoteError from the RPC handler
except rpc_common.RemoteError as e:
raise wsme.exc.ClientSideError(_(e.value))

View File

@ -894,6 +894,15 @@ class DockerRegistryCredentialNotFound(NotFound):
"for user %(name)s could not be found.")
class DockerRegistrySSLException(SysinvException):
message = _("Registry certificate signed by an unknown CA. "
"Install a trusted CA with 'system certificate-install -m ssl_ca'")
class DockerRegistryAPIException(SysinvException):
message = _("Error communicating with the docker registry")
class SDNNotEnabled(SysinvException):
message = _("SDN configuration is not enabled.")

View File

@ -11,7 +11,8 @@ import requests
from sysinv.common import constants
from sysinv.common import exception
CERT_PATH = '/etc/ssl/private/registry-cert.crt'
DOCKER_CERT_PATH = '/etc/ssl/private/registry-cert.crt'
SYSTEM_CERT_PATH = '/etc/ssl/certs/ca-bundle.crt'
KEYRING_SERVICE = 'CGCS'
REGISTRY_USERNAME = 'admin'
REGISTRY_BASEURL = 'https://%s/v2/' % constants.DOCKER_REGISTRY_SERVER
@ -68,10 +69,19 @@ def docker_registry_authenticate(www_authenticate):
# are passed as params
auth_string = base64.b64encode("%s:%s" % (REGISTRY_USERNAME, get_registry_password()))
token_server_request_headers = {"authorization": "Basic %s" % auth_string}
token_server_response = requests.get(auth_headers['realm'], verify=CERT_PATH,
params=auth_headers,
headers=token_server_request_headers)
# we need try twice.
# SYSTEM_CERT_PATH if the docker registry cert is signed by a trusted CA
# DOCKER_CERT_PATH if the registry certificate is self-signed
try:
token_server_response = requests.get(auth_headers['realm'],
verify=SYSTEM_CERT_PATH,
params=auth_headers,
headers=token_server_request_headers)
except requests.exceptions.SSLError:
token_server_response = requests.get(auth_headers['realm'],
verify=DOCKER_CERT_PATH,
params=auth_headers,
headers=token_server_request_headers)
if token_server_response.status_code == 200:
auth_headers['Authorization'] = "Bearer %s" % token_server_response.json().get("access_token")
@ -82,13 +92,23 @@ def docker_registry_get(path, registry_url=REGISTRY_BASEURL):
# we need to have this header to get the correct digest when giving the tag
headers = {"Accept": "application/vnd.docker.distribution.manifest.v2+json"}
resp = requests.get("%s%s" % (registry_url, path), verify=CERT_PATH, headers=headers)
try:
resp = requests.get("%s%s" % (registry_url, path),
verify=SYSTEM_CERT_PATH, headers=headers)
except requests.exceptions.SSLError:
resp = requests.get("%s%s" % (registry_url, path),
verify=DOCKER_CERT_PATH, headers=headers)
# authenticated registry, need to do auth with token server
if resp.status_code == 401:
auth_headers = docker_registry_authenticate(resp.headers["Www-Authenticate"])
headers.update(auth_headers)
resp = requests.get("%s%s" % (registry_url, path), verify=CERT_PATH, headers=headers)
try:
resp = requests.get("%s%s" % (registry_url, path),
verify=SYSTEM_CERT_PATH, headers=headers)
except requests.exceptions.SSLError:
resp = requests.get("%s%s" % (registry_url, path),
verify=DOCKER_CERT_PATH, headers=headers)
return resp
@ -96,12 +116,23 @@ def docker_registry_get(path, registry_url=REGISTRY_BASEURL):
def docker_registry_delete(path, registry_url=REGISTRY_BASEURL):
headers = {}
resp = requests.delete("%s%s" % (registry_url, path), verify=CERT_PATH, headers=headers)
try:
resp = requests.delete("%s%s" % (registry_url, path),
verify=SYSTEM_CERT_PATH, headers=headers)
except requests.exceptions.SSLError:
resp = requests.delete("%s%s" % (registry_url, path),
verify=DOCKER_CERT_PATH, headers=headers)
# authenticated registry, need to do auth with token server
if resp.status_code == 401:
auth_headers = docker_registry_authenticate(resp.headers["Www-Authenticate"])
headers.update(auth_headers)
resp = requests.delete("%s%s" % (registry_url, path), verify=CERT_PATH, headers=headers)
try:
resp = requests.delete("%s%s" % (registry_url, path),
verify=SYSTEM_CERT_PATH, headers=headers)
except requests.exceptions.SSLError:
resp = requests.delete("%s%s" % (registry_url, path),
verify=DOCKER_CERT_PATH, headers=headers)
return resp

View File

@ -37,6 +37,7 @@ import hashlib
import math
import os
import re
import requests
import shutil
import socket
import tempfile
@ -1443,7 +1444,15 @@ class ConductorManager(service.PeriodicService):
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
def docker_registry_image_list(self, context):
image_list_response = docker_registry.docker_registry_get("_catalog")
try:
image_list_response = docker_registry.docker_registry_get("_catalog")
except requests.exceptions.SSLError:
LOG.exception("Failed to get docker registry catalog")
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to get docker registry catalog")
raise exception.DockerRegistryAPIException()
if image_list_response.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% image_list_response.status_code)
@ -1465,8 +1474,15 @@ class ConductorManager(service.PeriodicService):
return images
def docker_registry_image_tags(self, context, image_name):
image_tags_response = docker_registry.docker_registry_get(
"%s/tags/list" % image_name)
try:
image_tags_response = docker_registry.docker_registry_get(
"%s/tags/list" % image_name)
except requests.exceptions.SSLError:
LOG.exception("Failed to get docker registry image tags")
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to get docker registry image tags")
raise exception.DockerRegistryAPIException()
if image_tags_response.status_code != 200:
LOG.error("Bad response from docker registry: %s"
@ -1494,8 +1510,17 @@ class ConductorManager(service.PeriodicService):
image_name_and_tag = image_name_and_tag.split(":")
# first get the image digest for the image name and tag provided
digest_resp = docker_registry.docker_registry_get("%s/manifests/%s"
% (image_name_and_tag[0], image_name_and_tag[1]))
try:
digest_resp = docker_registry.docker_registry_get("%s/manifests/%s"
% (image_name_and_tag[0], image_name_and_tag[1]))
except requests.exceptions.SSLError:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistryAPIException()
if digest_resp.status_code != 200:
LOG.error("Bad response from docker registry: %s"
@ -1505,8 +1530,17 @@ class ConductorManager(service.PeriodicService):
image_digest = digest_resp.headers['Docker-Content-Digest']
# now delete the image
image_delete_response = docker_registry.docker_registry_delete(
"%s/manifests/%s" % (image_name_and_tag[0], image_digest))
try:
image_delete_response = docker_registry.docker_registry_delete(
"%s/manifests/%s" % (image_name_and_tag[0], image_digest))
except requests.exceptions.SSLError:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistryAPIException()
if image_delete_response.status_code != 202:
LOG.error("Bad response from docker registry: %s"