Merge "Encrypt certs and keys"

This commit is contained in:
Zuul 2019-03-06 02:38:21 +00:00 committed by Gerrit Code Review
commit e74f19bc0b
14 changed files with 105 additions and 80 deletions

View File

@ -325,6 +325,7 @@ function octavia_configure {
iniset $OCTAVIA_CONF certificates ca_certificate ${OCTAVIA_CERTS_DIR}/ca_01.pem
iniset $OCTAVIA_CONF certificates ca_private_key ${OCTAVIA_CERTS_DIR}/private/cakey.pem
iniset $OCTAVIA_CONF certificates ca_private_key_passphrase foobar
iniset $OCTAVIA_CONF certificates server_certs_key_passphrase insecure-key-do-not-use-this-key
if [[ "$OCTAVIA_USE_LEGACY_RBAC" == "True" ]]; then
cp $OCTAVIA_DIR/etc/policy/admin_or_owner-policy.json $OCTAVIA_CONF_DIR/policy.json

View File

@ -29,6 +29,9 @@ TLS_KEY_DEFAULT = os.environ.get(
'OS_OCTAVIA_TLS_CA_KEY', '/etc/ssl/private/ssl-cert-snakeoil.key'
)
TLS_PKP_DEFAULT = os.environ.get('OS_OCTAVIA_CA_KEY_PASS')
TLS_PASS_AMPS_DEFAULT = os.environ.get('TLS_PASS_AMPS_DEFAULT',
'insecure-key-do-not-use-this-key')
TLS_DIGEST_DEFAULT = os.environ.get('OS_OCTAVIA_CA_SIGNING_DIGEST', 'sha256')
TLS_STORAGE_DEFAULT = os.environ.get(
'OS_OCTAVIA_TLS_STORAGE', '/var/lib/octavia/certificates/'
@ -47,6 +50,12 @@ certgen_opts = [
default=TLS_PKP_DEFAULT,
help='Passphrase for the Private Key. Defaults'
' to env[OS_OCTAVIA_CA_KEY_PASS] or None.'),
cfg.StrOpt('server_certs_key_passphrase',
default=TLS_PASS_AMPS_DEFAULT,
help='Passphrase for encrypting Amphora Certificates and '
'Private Keys. Defaults to env[TLS_PASS_AMPS_DEFAULT] or '
'insecure-key-do-not-use-this-key',
required=True),
cfg.StrOpt('signing_digest',
default=TLS_DIGEST_DEFAULT,
help='Certificate signing digest. Defaults'
@ -60,10 +69,6 @@ certmgr_opts = [
'Defaults to env[OS_OCTAVIA_TLS_STORAGE].')
]
CONF = cfg.CONF
CONF.register_opts(certgen_opts, group='certificates')
CONF.register_opts(certmgr_opts, group='certificates')
class LocalCert(cert.Cert):
"""Representation of a Cert for local storage."""

View File

@ -26,6 +26,7 @@ from oslo_db import options as db_options
from oslo_log import log as logging
import oslo_messaging as messaging
from octavia.certificates.common import local
from octavia.common import constants
from octavia.common import utils
from octavia.i18n import _
@ -621,6 +622,9 @@ cfg.CONF.register_opts(neutron_opts, group='neutron')
cfg.CONF.register_opts(quota_opts, group='quotas')
cfg.CONF.register_opts(audit_opts, group='audit')
cfg.CONF.register_opts(local.certgen_opts, group='certificates')
cfg.CONF.register_opts(local.certmgr_opts, group='certificates')
# Ensure that the control exchange is set correctly
messaging.set_transport_defaults(control_exchange='octavia')
_SQL_CONNECTION_DEFAULT = 'sqlite://'

View File

@ -26,6 +26,7 @@ import netaddr
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import six
from stevedore import driver as stevedore_driver
CONF = cfg.CONF
@ -90,6 +91,19 @@ def ip_netmask_to_cidr(ip, netmask):
return "{ip}/{netmask}".format(ip=net.network, netmask=net.prefixlen)
def get_six_compatible_value(value, six_type=six.string_types):
if six.PY3 and isinstance(value, six_type):
value = value.encode('utf-8')
return value
def get_six_compatible_server_certs_key_passphrase():
key = CONF.certificates.server_certs_key_passphrase
if six.PY3 and isinstance(key, six.string_types):
key = key.encode('utf-8')
return base64.urlsafe_b64encode(key)
class exception_logger(object):
"""Wrap a function and log raised exception

View File

@ -71,23 +71,6 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
self._l7rule_repo = repo.L7RuleRepository()
self._flavor_repo = repo.FlavorRepository()
self._exclude_result_logging_tasks = (
constants.ROLE_STANDALONE + '-' +
constants.CREATE_AMP_FOR_LB_SUBFLOW + '-' +
constants.GENERATE_SERVER_PEM,
constants.ROLE_BACKUP + '-' +
constants.CREATE_AMP_FOR_LB_SUBFLOW + '-' +
constants.GENERATE_SERVER_PEM,
constants.ROLE_MASTER + '-' +
constants.CREATE_AMP_FOR_LB_SUBFLOW + '-' +
constants.GENERATE_SERVER_PEM,
constants.GENERATE_SERVER_PEM_TASK,
constants.FAILOVER_AMPHORA_FLOW + '-' +
constants.CREATE_AMP_FOR_LB_SUBFLOW + '-' +
constants.GENERATE_SERVER_PEM,
constants.CREATE_AMP_FOR_LB_SUBFLOW + '-' +
constants.UPDATE_CERT_EXPIRATION)
super(ControllerWorker, self).__init__()
@tenacity.retry(
@ -115,10 +98,7 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
constants.LB_CREATE_SPARES_POOL_PRIORITY,
constants.FLAVOR: None}
)
with tf_logging.DynamicLoggingListener(
create_amp_tf, log=LOG,
hide_inputs_outputs_of=self._exclude_result_logging_tasks):
with tf_logging.DynamicLoggingListener(create_amp_tf, log=LOG):
create_amp_tf.run()
return create_amp_tf.storage.fetch('amphora')
@ -359,9 +339,7 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
topology=topology, listeners=lb.listeners)
create_lb_tf = self._taskflow_load(create_lb_flow, store=store)
with tf_logging.DynamicLoggingListener(
create_lb_tf, log=LOG,
hide_inputs_outputs_of=self._exclude_result_logging_tasks):
with tf_logging.DynamicLoggingListener(create_lb_tf, log=LOG):
create_lb_tf.run()
def delete_load_balancer(self, load_balancer_id, cascade=False):
@ -857,10 +835,7 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
role=amp.role, load_balancer=lb),
store=stored_params)
with tf_logging.DynamicLoggingListener(
failover_amphora_tf, log=LOG,
hide_inputs_outputs_of=self._exclude_result_logging_tasks):
with tf_logging.DynamicLoggingListener(failover_amphora_tf, log=LOG):
failover_amphora_tf.run()
def failover_amphora(self, amphora_id):

View File

@ -13,6 +13,7 @@
# under the License.
#
from cryptography import fernet
from oslo_config import cfg
from oslo_log import log as logging
import six
@ -23,6 +24,7 @@ from taskflow.types import failure
from octavia.amphorae.backends.agent import agent_jinja_cfg
from octavia.amphorae.driver_exceptions import exceptions as driver_except
from octavia.common import constants
from octavia.common import utils
from octavia.controller.worker import task_utils as task_utilities
from octavia.db import api as db_apis
from octavia.db import repositories as repo
@ -273,7 +275,9 @@ class AmphoraCertUpload(BaseAmphoraTask):
def execute(self, amphora, server_pem):
"""Execute cert_update_amphora routine."""
LOG.debug("Upload cert in amphora REST driver")
self.amphora_driver.upload_cert_amp(amphora, server_pem)
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
self.amphora_driver.upload_cert_amp(amphora, fer.decrypt(server_pem))
class AmphoraUpdateVRRPInterface(BaseAmphoraTask):

View File

@ -13,10 +13,12 @@
# under the License.
#
from cryptography import fernet
from oslo_config import cfg
from stevedore import driver as stevedore_driver
from taskflow import task
from octavia.common import utils
CONF = cfg.CONF
CERT_VALIDITY = 2 * 365 * 24 * 60 * 60
@ -44,5 +46,7 @@ class GenerateServerPEMTask(BaseCertTask):
cert = self.cert_generator.generate_cert_key_pair(
cn=amphora_id,
validity=CERT_VALIDITY)
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
return cert.certificate + cert.private_key
return fer.encrypt(cert.certificate + cert.private_key)

View File

@ -15,6 +15,7 @@
import time
from cryptography import fernet
from oslo_config import cfg
from oslo_log import log as logging
from stevedore import driver as stevedore_driver
@ -25,6 +26,7 @@ from octavia.amphorae.backends.agent import agent_jinja_cfg
from octavia.common import constants
from octavia.common import exceptions
from octavia.common.jinja import user_data_jinja_cfg
from octavia.common import utils
from octavia.controller.worker import amphora_rate_limit
CONF = cfg.CONF
@ -144,8 +146,11 @@ class CertComputeCreate(ComputeCreate):
# load client certificate
with open(CONF.controller_worker.client_ca, 'r') as client_ca:
ca = client_ca.read()
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
config_drive_files = {
'/etc/octavia/certs/server.pem': server_pem,
'/etc/octavia/certs/server.pem': fer.decrypt(server_pem),
'/etc/octavia/certs/client_ca.pem': ca}
return super(CertComputeCreate, self).execute(
amphora_id, config_drive_files=config_drive_files,

View File

@ -13,6 +13,7 @@
# under the License.
#
from cryptography import fernet
from oslo_config import cfg
from oslo_db import exception as odb_exceptions
from oslo_log import log as logging
@ -27,6 +28,7 @@ from taskflow.types import failure
from octavia.common import constants
from octavia.common import data_models
import octavia.common.tls_utils.cert_parser as cert_parser
from octavia.common import utils
from octavia.common import validate
from octavia.controller.worker import task_utils as task_utilities
from octavia.db import api as db_apis
@ -918,7 +920,11 @@ class UpdateAmphoraDBCertExpiration(BaseDatabaseTask):
"""
LOG.debug("Update DB cert expiry date of amphora id: %s", amphora_id)
cert_expiration = cert_parser.get_cert_expiration(server_pem)
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
cert_expiration = cert_parser.get_cert_expiration(
fer.decrypt(server_pem))
LOG.debug("Certificate expiration date is %s ", cert_expiration)
self.amphora_repo.update(db_apis.get_session(), amphora_id,
cert_expiration=cert_expiration)

View File

@ -13,6 +13,7 @@
# under the License.
#
from cryptography import fernet
import mock
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
@ -22,6 +23,7 @@ from taskflow.types import failure
from octavia.amphorae.driver_exceptions import exceptions as driver_except
from octavia.common import constants
from octavia.common import data_models
from octavia.common import utils
from octavia.controller.worker.tasks import amphora_driver_tasks
from octavia.db import repositories as repo
import octavia.tests.unit.base as base
@ -506,12 +508,15 @@ class TestAmphoraDriverTasks(base.TestCase):
mock_listener_repo_get,
mock_listener_repo_update,
mock_amphora_repo_update):
pem_file_mock = 'test-perm-file'
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
pem_file_mock = fer.encrypt(
utils.get_six_compatible_value('test-pem-file'))
amphora_cert_upload_mock = amphora_driver_tasks.AmphoraCertUpload()
amphora_cert_upload_mock.execute(_amphora_mock, pem_file_mock)
mock_driver.upload_cert_amp.assert_called_once_with(
_amphora_mock, pem_file_mock)
_amphora_mock, fer.decrypt(pem_file_mock))
def test_amphora_update_vrrp_interface(self,
mock_driver,

View File

@ -13,21 +13,31 @@
# under the License.
#
from cryptography import fernet
import mock
from octavia.certificates.common import local
from octavia.common import utils
from octavia.controller.worker.tasks import cert_task
import octavia.tests.unit.base as base
class TestCertTasks(base.TestCase):
@mock.patch('stevedore.driver.DriverManager.driver')
def test_execute(self, mock_driver):
dummy_cert = local.LocalCert('test_cert', 'test_key')
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
dummy_cert = local.LocalCert(
utils.get_six_compatible_value('test_cert'),
utils.get_six_compatible_value('test_key'))
mock_driver.generate_cert_key_pair.side_effect = [dummy_cert]
c = cert_task.GenerateServerPEMTask()
pem = c.execute('123')
self.assertEqual(
pem, dummy_cert.get_certificate() + dummy_cert.get_private_key())
fer.decrypt(pem),
dummy_cert.get_certificate() +
dummy_cert.get_private_key()
)
mock_driver.generate_cert_key_pair.assert_called_once_with(
cn='123', validity=cert_task.CERT_VALIDITY)

View File

@ -13,6 +13,7 @@
# under the License.
#
from cryptography import fernet
import mock
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
@ -20,6 +21,7 @@ from oslo_utils import uuidutils
from octavia.common import constants
from octavia.common import exceptions
from octavia.common import utils
from octavia.controller.worker.tasks import compute_tasks
from octavia.tests.common import utils as test_utils
import octavia.tests.unit.base as base
@ -270,13 +272,17 @@ class TestComputeTasks(base.TestCase):
@mock.patch('stevedore.driver.DriverManager.driver')
def test_compute_create_cert(self, mock_driver, mock_conf, mock_jinja):
createcompute = compute_tasks.CertComputeCreate()
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
mock_driver.build.return_value = COMPUTE_ID
path = '/etc/octavia/certs/ca_01.pem'
self.useFixture(test_utils.OpenFixture(path, 'test'))
# Test execute()
compute_id = createcompute.execute(_amphora_mock.id, 'test_cert',
test_cert = fer.encrypt(
utils.get_six_compatible_value('test_cert')
)
compute_id = createcompute.execute(_amphora_mock.id, test_cert,
server_group_id=SERVER_GRPOUP_ID
)
@ -293,7 +299,7 @@ class TestComputeTasks(base.TestCase):
port_ids=[],
user_data=None,
config_drive_files={
'/etc/octavia/certs/server.pem': 'test_cert',
'/etc/octavia/certs/server.pem': fer.decrypt(test_cert),
'/etc/octavia/certs/client_ca.pem': 'test',
'/etc/octavia/amphora-agent.conf': 'test_conf'},
server_group_id=SERVER_GRPOUP_ID)
@ -307,7 +313,7 @@ class TestComputeTasks(base.TestCase):
self.assertRaises(TypeError,
createcompute.execute,
_amphora_mock,
config_drive_files='test_cert')
config_drive_files=test_cert)
# Test revert()

View File

@ -15,6 +15,7 @@
import random
from cryptography import fernet
import mock
from oslo_db import exception as odb_exceptions
from oslo_utils import uuidutils
@ -23,6 +24,7 @@ from taskflow.types import failure
from octavia.common import constants
from octavia.common import data_models
from octavia.common import utils
from octavia.controller.worker.tasks import database_tasks
from octavia.db import repositories as repo
import octavia.tests.unit.base as base
@ -83,42 +85,6 @@ _vip_mock.subnet_id = SUBNET_ID
_vip_mock.ip_address = VIP_IP
_vrrp_group_mock = mock.MagicMock()
_cert_mock = mock.MagicMock()
_pem_mock = """Junk
-----BEGIN CERTIFICATE-----
MIIBhDCCAS6gAwIBAgIGAUo7hO/eMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMT
BElNRDIwHhcNMTQxMjExMjI0MjU1WhcNMjUxMTIzMjI0MjU1WjAPMQ0wCwYDVQQD
EwRJTUQzMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKHIPXo2pfD5dpnpVDVz4n43
zn3VYsjz/mgOZU0WIWjPA97mvulb7mwb4/LB4ijOMzHj9XfwP75GiOFxYFs8O80C
AwEAAaNwMG4wDwYDVR0TAQH/BAUwAwEB/zA8BgNVHSMENTAzgBS6rfnABCO3oHEz
NUUtov2hfXzfVaETpBEwDzENMAsGA1UEAxMESU1EMYIGAUo7hO/DMB0GA1UdDgQW
BBRiLW10LVJiFO/JOLsQFev0ToAcpzANBgkqhkiG9w0BAQsFAANBABtdF+89WuDi
TC0FqCocb7PWdTucaItD9Zn55G8KMd93eXrOE/FQDf1ScC+7j0jIHXjhnyu6k3NV
8el/x5gUHlc=
-----END CERTIFICATE-----
Junk should be ignored by x509 splitter
-----BEGIN CERTIFICATE-----
MIIBhDCCAS6gAwIBAgIGAUo7hO/DMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMT
BElNRDEwHhcNMTQxMjExMjI0MjU1WhcNMjUxMTIzMjI0MjU1WjAPMQ0wCwYDVQQD
EwRJTUQyMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJYHqnsisVKTlwVaCSa2wdrv
CeJJzqpEVV0RVgAAF6FXjX2Tioii+HkXMR9zFgpE1w4yD7iu9JDb8yTdNh+NxysC
AwEAAaNwMG4wDwYDVR0TAQH/BAUwAwEB/zA8BgNVHSMENTAzgBQt3KvN8ncGj4/s
if1+wdvIMCoiE6ETpBEwDzENMAsGA1UEAxMEcm9vdIIGAUo7hO+mMB0GA1UdDgQW
BBS6rfnABCO3oHEzNUUtov2hfXzfVTANBgkqhkiG9w0BAQsFAANBAIlJODvtmpok
eoRPOb81MFwPTTGaIqafebVWfBlR0lmW8IwLhsOUdsQqSzoeypS3SJUBpYT1Uu2v
zEDOmgdMsBY=
-----END CERTIFICATE-----
Junk should be thrown out like junk
-----BEGIN CERTIFICATE-----
MIIBfzCCASmgAwIBAgIGAUo7hO+mMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMT
BHJvb3QwHhcNMTQxMjExMjI0MjU1WhcNMjUxMTIzMjI0MjU1WjAPMQ0wCwYDVQQD
EwRJTUQxMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAI+tSJxr60ogwXFmgqbLMW7K
3fkQnh9sZBi7Qo6AzUnfe/AhXoisib651fOxKXCbp57IgzLTv7O9ygq3I+5fQqsC
AwEAAaNrMGkwDwYDVR0TAQH/BAUwAwEB/zA3BgNVHSMEMDAugBR73ZKSpjbsz9tZ
URkvFwpIO7gB4KETpBEwDzENMAsGA1UEAxMEcm9vdIIBATAdBgNVHQ4EFgQULdyr
zfJ3Bo+P7In9fsHbyDAqIhMwDQYJKoZIhvcNAQELBQADQQBenkZ2k7RgZqgj+dxA
D7BF8MN1oUAOpyYqAjkGddSEuMyNmwtHKZI1dyQ0gBIQdiU9yAG2oTbUIK4msbBV
uJIQ
-----END CERTIFICATE-----"""
_compute_mock = mock.MagicMock()
_compute_mock.lb_network_ip = LB_NET_IP
_compute_mock.cached_zone = CACHED_ZONE
@ -1095,6 +1061,11 @@ class TestDatabaseTasks(base.TestCase):
mock_get_cert_exp):
update_amp_cert = database_tasks.UpdateAmphoraDBCertExpiration()
key = utils.get_six_compatible_server_certs_key_passphrase()
fer = fernet.Fernet(key)
_pem_mock = fer.encrypt(
utils.get_six_compatible_value('test_cert')
)
update_amp_cert.execute(_amphora_mock.id, _pem_mock)
repo.AmphoraRepository.update.assert_called_once_with(

View File

@ -0,0 +1,15 @@
---
security:
- |
As a followup to the fix that resolved CVE-2018-16856, Octavia will now
encrypt certificates and keys used for secure communication with amphorae,
in its internal workflows. Octavia used to exclude debug-level log prints
for specific tasks and flows that were explicitly specified by name, a
method that is susceptive to code changes.
other:
- |
Added a new option named server_certs_key_passphrase under the certificates
section. The default value gets copied from an environment variable named
TLS_PASS_AMPS_DEFAULT. In a case where TLS_PASS_AMPS_DEFAULT is not set,
and the operator did not fill any other value directly,
'insecure-key-do-not-use-this-key' will be used.