Make Kubernetes API call secure
This patch makes API call to Kubernetes secure using the certificate and key. Co-Authored-By: Madhuri Kumari<madhuri.kumari@intel.com> Co-Authored-By: OTSUKA, Yuanying<yuanying@fraction.jp> Depends-On: I76b0f91f0c44f9880980e35c6b8856ea48ed3ce1 Change-Id: Id4dceb83f67b80f5b39e3047011f9e34e840359d Partially-Implements: blueprint secure-kubernetes
This commit is contained in:
parent
30b3d99d5c
commit
ec2cd88a05
|
@ -82,7 +82,8 @@ class RESTClientObject(object):
|
|||
key_file=key_file,
|
||||
cert_file=cert_file,
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs=ca_certs)
|
||||
ca_certs=ca_certs,
|
||||
assert_hostname=False)
|
||||
|
||||
def agent(self, url):
|
||||
"""
|
||||
|
|
|
@ -12,28 +12,87 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from oslo_log import log as logging
|
||||
|
||||
from magnum.common import cert_manager
|
||||
from magnum.common.pythonk8sclient.swagger_client import api_client
|
||||
from magnum.common.pythonk8sclient.swagger_client.apis import apiv_api
|
||||
from magnum.conductor import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class K8sAPI(apiv_api.ApivApi):
|
||||
|
||||
def _create_temp_file_with_content(self, content):
|
||||
"""Creates temp file and write content to the file.
|
||||
|
||||
:param content: file content
|
||||
:returns: temp file
|
||||
"""
|
||||
try:
|
||||
tmp = NamedTemporaryFile(delete=True)
|
||||
tmp.write(content)
|
||||
tmp.flush()
|
||||
except Exception as err:
|
||||
LOG.error("Error while creating temp file: %s" % err)
|
||||
raise err
|
||||
return tmp
|
||||
|
||||
def __init__(self, context, obj):
|
||||
# retrieve the URL of the k8s API endpoint
|
||||
k8s_api_endpoint = self._retrieve_k8s_api_endpoint(context, obj)
|
||||
self.ca_file = None
|
||||
self.cert_file = None
|
||||
self.key_file = None
|
||||
|
||||
bay = utils.retrieve_bay(context, obj)
|
||||
if bay.magnum_cert_ref:
|
||||
self._create_certificate_files(bay)
|
||||
|
||||
# build a connection with Kubernetes master
|
||||
client = api_client.ApiClient(k8s_api_endpoint)
|
||||
client = api_client.ApiClient(bay.api_address,
|
||||
key_file=self.key_file.name,
|
||||
cert_file=self.cert_file.name,
|
||||
ca_certs=self.ca_file.name)
|
||||
|
||||
super(K8sAPI, self).__init__(client)
|
||||
|
||||
@staticmethod
|
||||
def _retrieve_k8s_api_endpoint(context, obj):
|
||||
if hasattr(obj, 'bay_uuid'):
|
||||
obj = utils.retrieve_bay(context, obj)
|
||||
def _create_certificate_files(self, bay):
|
||||
"""Read certificate and key for a bay and stores in files.
|
||||
|
||||
return obj.api_address
|
||||
:param bay: Bay object
|
||||
"""
|
||||
magnum_cert_obj = cert_manager.get_backend().CertManager.get_cert(
|
||||
bay.magnum_cert_ref)
|
||||
self.cert_file = self._create_temp_file_with_content(
|
||||
magnum_cert_obj.certificate)
|
||||
private_key = serialization.load_pem_private_key(
|
||||
magnum_cert_obj.private_key,
|
||||
password=magnum_cert_obj.private_key_passphrase,
|
||||
backend=default_backend(),
|
||||
)
|
||||
private_key = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption())
|
||||
self.key_file = self._create_temp_file_with_content(
|
||||
private_key)
|
||||
ca_cert_obj = cert_manager.get_backend().CertManager.get_cert(
|
||||
bay.ca_cert_ref)
|
||||
self.ca_file = self._create_temp_file_with_content(
|
||||
ca_cert_obj.certificate)
|
||||
|
||||
def __del__(self):
|
||||
if self.ca_file:
|
||||
self.ca_file.close()
|
||||
if self.cert_file:
|
||||
self.cert_file.close()
|
||||
if self.key_file:
|
||||
self.key_file.close()
|
||||
|
||||
|
||||
def create_k8s_api(context, obj):
|
||||
|
|
|
@ -720,6 +720,13 @@ class Connection(object):
|
|||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
|
||||
def get_x509keypair_by_bay_uuid(self, bay_uuid):
|
||||
"""Returns the cert for a given bay.
|
||||
|
||||
:param bay_uuid: The uuid of a bay.
|
||||
:returns: A cert.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_magnum_service(self, magnum_service_id):
|
||||
"""Destroys a magnum_service record.
|
||||
|
|
|
@ -985,6 +985,13 @@ class Connection(api.Connection):
|
|||
return _paginate_query(models.X509KeyPair, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
def get_x509keypair_by_bay_uuid(self, context, bay_uuid):
|
||||
query = model_query(models.X509KeyPair).filter_by(bay_uuid=bay_uuid)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.BayNotFound(bay=bay_uuid)
|
||||
|
||||
def destroy_magnum_service(self, magnum_service_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
|
|
|
@ -22,7 +22,8 @@ from magnum.objects import base
|
|||
class X509KeyPair(base.MagnumPersistentObject, base.MagnumObject,
|
||||
base.MagnumObjectDictCompat):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added new method get_x509keypair_by_bay_uuid
|
||||
VERSION = '1.1'
|
||||
|
||||
dbapi = dbapi.get_instance()
|
||||
|
||||
|
@ -199,3 +200,14 @@ class X509KeyPair(base.MagnumPersistentObject, base.MagnumObject,
|
|||
for field in self.fields:
|
||||
if self.obj_attr_is_set(field) and self[field] != current[field]:
|
||||
self[field] = current[field]
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_bay_uuid(cls, context, bay_uuid):
|
||||
"""Return a :class:`Cert` object associated with a given bay.
|
||||
|
||||
:param bay_uuid: the uuid of a bay.
|
||||
:param context: Security context
|
||||
:returns: a class:`Cert` object.
|
||||
"""
|
||||
db_cert = cls.dbapi.get_x509keypair_by_bay_uuid(context, bay_uuid)
|
||||
return X509KeyPair._from_db_object(cls(context), db_cert)
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
from mock import patch
|
||||
|
||||
from magnum.conductor import k8s_api
|
||||
|
@ -20,26 +21,97 @@ from magnum.tests import base
|
|||
|
||||
|
||||
class TestK8sAPI(base.TestCase):
|
||||
content_dict = {
|
||||
'fake-magnum-cert-ref': {
|
||||
'certificate': 'certificate-content',
|
||||
'private_key': 'private-key-content'
|
||||
},
|
||||
'fake-ca-cert-ref': {
|
||||
'certificate': 'ca-cert-content',
|
||||
'private_key': None
|
||||
}
|
||||
}
|
||||
file_dict = {
|
||||
'ca-cert-content': mock.MagicMock(),
|
||||
'certificate-content': mock.MagicMock(),
|
||||
'private-key-content': mock.MagicMock()
|
||||
}
|
||||
file_name = {
|
||||
'ca-cert-content': 'ca-cert-temp-file-name',
|
||||
'certificate-content': 'cert-temp-file-name',
|
||||
'private-key-content': 'priv-key-temp-file-name'
|
||||
}
|
||||
|
||||
@patch('magnum.objects.Bay.get_by_uuid')
|
||||
def test_retrieve_k8s_api_endpoint(self, mock_bay_get_by_uuid):
|
||||
expected_context = 'context'
|
||||
expected_api_address = 'api_address'
|
||||
def _mock_named_file_creation(self, content):
|
||||
return TestK8sAPI.file_dict[content]
|
||||
|
||||
resource = objects.Pod({})
|
||||
resource.bay_uuid = 'bay_uuid'
|
||||
bay = objects.Bay({})
|
||||
bay.api_address = expected_api_address
|
||||
def _mock_cert_mgr_get_cert(self, cert_ref):
|
||||
cert_obj = mock.MagicMock()
|
||||
cert_obj.certificate = TestK8sAPI.content_dict[cert_ref]['certificate']
|
||||
cert_obj.private_key = TestK8sAPI.content_dict[cert_ref]['private_key']
|
||||
return cert_obj
|
||||
|
||||
mock_bay_get_by_uuid.return_value = bay
|
||||
@patch('magnum.conductor.k8s_api.serialization.load_pem_private_key')
|
||||
@patch('magnum.conductor.utils.retrieve_bay')
|
||||
@patch('magnum.common.pythonk8sclient.swagger_client.api_client.ApiClient')
|
||||
@patch(
|
||||
'magnum.common.pythonk8sclient.swagger_client.apis.apiv_api.ApivApi')
|
||||
def _test_create_k8s_api(self, cls,
|
||||
mock_api_vapi,
|
||||
mock_api_client,
|
||||
mock_bay_retrieval,
|
||||
mock_load_pem_private_key):
|
||||
bay_obj = mock.MagicMock()
|
||||
bay_obj.uuid = 'bay-uuid'
|
||||
bay_obj.api_address = 'fake-k8s-api-endpoint'
|
||||
bay_obj.magnum_cert_ref = 'fake-magnum-cert-ref'
|
||||
bay_obj.ca_cert_ref = 'fake-ca-cert-ref'
|
||||
mock_bay_retrieval.return_value = bay_obj
|
||||
mock_private_bytes = mock.MagicMock()
|
||||
mock_load_pem_private_key.return_value = mock_private_bytes
|
||||
mock_private_bytes.private_bytes = mock.MagicMock(
|
||||
return_value='private-key-content')
|
||||
|
||||
actual_api_endpoint = k8s_api.K8sAPI._retrieve_k8s_api_endpoint(
|
||||
expected_context, resource)
|
||||
self.assertEqual(expected_api_address, actual_api_endpoint)
|
||||
file_dict = TestK8sAPI.file_dict
|
||||
for content in file_dict.keys():
|
||||
file_hdl = file_dict[content]
|
||||
file_hdl.name = TestK8sAPI.file_name[content]
|
||||
|
||||
@patch('magnum.conductor.k8s_api.K8sAPI')
|
||||
def test_create_k8s_api(self, mock_k8s_api_cls):
|
||||
context = 'context'
|
||||
bay = objects.Bay({})
|
||||
k8s_api.create_k8s_api(context, bay)
|
||||
mock_k8s_api_cls.assert_called_once_with(context, bay)
|
||||
|
||||
obj = getattr(objects, cls)({})
|
||||
self.assertFalse(hasattr(obj, 'bay_uuid'))
|
||||
obj.bay_uuid = 'bay-uuid'
|
||||
|
||||
with patch(
|
||||
'magnum.conductor.k8s_api.K8sAPI._create_temp_file_with_content',
|
||||
side_effect=self._mock_named_file_creation):
|
||||
with patch(
|
||||
'magnum.common.cert_manager.local_cert_manager'
|
||||
'.CertManager.get_cert',
|
||||
side_effect=self._mock_cert_mgr_get_cert):
|
||||
with patch(
|
||||
'magnum.common.cert_manager.barbican_cert_manager'
|
||||
'.CertManager.get_cert',
|
||||
side_effect=self._mock_cert_mgr_get_cert):
|
||||
k8s_api.create_k8s_api(context, obj)
|
||||
|
||||
mock_bay_retrieval.assert_called_once_with(context, obj)
|
||||
|
||||
mock_api_client.assert_called_once_with(
|
||||
bay_obj.api_address,
|
||||
key_file='priv-key-temp-file-name',
|
||||
cert_file='cert-temp-file-name',
|
||||
ca_certs='ca-cert-temp-file-name')
|
||||
|
||||
def test_create_k8s_api_with_service(self):
|
||||
self._test_create_k8s_api('Service')
|
||||
|
||||
def test_create_k8s_api_with_bay(self):
|
||||
self._test_create_k8s_api('Bay')
|
||||
|
||||
def test_create_k8s_api_with_pod(self):
|
||||
self._test_create_k8s_api('Pod')
|
||||
|
||||
def test_create_k8s_api_with_rc(self):
|
||||
self._test_create_k8s_api('ReplicationController')
|
||||
|
|
|
@ -434,7 +434,7 @@ object_data = {
|
|||
'Pod': '1.1-7a31c372f163742845c10a008f47cc15',
|
||||
'ReplicationController': '1.0-782b7deb9307b2807101541b7e58b8a2',
|
||||
'Service': '1.0-a8cf7e95fced904419164dbcb6d32b38',
|
||||
'X509KeyPair': '1.0-fd008eba0fbc390e0e5da247bba4eedd',
|
||||
'X509KeyPair': '1.1-4aecc268e23e32b8a762d43ba1a4b159',
|
||||
'MagnumService': '1.0-2d397ec59b0046bd5ec35cd3e06efeca',
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue