Anchor support to Octavia

Use Anchor for certificate signing to make the octavia communication
more secure. Anchor Ref url: https://github.com/openstack/anchor

Co-Authored-By: bharath <bharath.stacker@gmail.com>
Co-Authored-By: German Eichberger <german.eichberger@hp.com>

Change-Id: Id77b2b1540377db661f15d4eeafc4922f446d987
This commit is contained in:
bharath 2015-11-05 16:12:13 -08:00 committed by German Eichberger
parent 0a1d45f696
commit d2072ae0ae
10 changed files with 297 additions and 85 deletions

View File

@ -0,0 +1,22 @@
======
Anchor
======
Anchor (see https://wiki.openstack.org/wiki/Security/Projects/Anchor) is
an ephemeral PKI system built to enable cryptographic trust in OpenStack
services. In the context of Octavia it can be used to sign the certificates
which secure the amphora - controller communication.
Basic Setup
-----------
# Download/Install/Start Anchor from https://github.com/openstack/anchor
# Change the listening port in config.py to 9999
# I found it useful to run anchor in an additional devstack screen
# Set in octavia.conf
## [controller_worker] cert_generator to anchor
## [haproxy_amphora] server_ca = /opt/stack/anchor/CA/root-ca.crt (Anchor CA)
# Restart o-cw o-hm o-hk
Benefit
-------
In bigger cloud installations Anchor can be a gateway to a more secure
certificate management system than our default local signing.

View File

@ -28,6 +28,12 @@ description of these terms.
back-end amphora corresponding with the driver. This communication
happens over the LB network.
Anchor
Is an OpenStack project for an ephemeral PKI system (see
https://wiki.openstack.org/wiki/Security/Projects/Anchor). In Octavia
we can use Anchor to sign the certificates we use to authenticate/secure
controller <-> amphora communication.
Apolocation
Term used to describe when two or more amphorae are not colocated on
the same physical hardware (which is often essential in HA topologies).

View File

@ -73,6 +73,12 @@
# barbican_cert_manager
# cert_manager=local_cert_manager
[anchor]
# Use OpenStack anchor to sign the amphora REST API certificates
# url = http://localhost:9999/v1/sign/default
# username = myusername
# password = simplepassword
[networking]
# Network to communicate with amphora
# lb_network_name =
@ -137,6 +143,7 @@
#
# Certificate Generator options are local_cert_generator
# barbican_cert_generator
# anchor_cert_generator
# cert_generator = local_cert_generator
[task_flow]

View File

@ -0,0 +1,68 @@
# Copyright (c) 2015 Hewlett Packard Enterprise Development Company LP
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_log import log as logging
import requests
from octavia.certificates.generator import local
from octavia.common import exceptions
from octavia.i18n import _LE
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_group('anchor', 'octavia.common.config')
class AnchorException(exceptions.CertificateGenerationException):
pass
class AnchorCertGenerator(local.LocalCertGenerator):
"""Cert Generator Interface that signs certs with Anchor."""
@classmethod
def sign_cert(cls, csr, validity=None, **kwargs):
"""Signs a certificate using Anchor based on the specified CSR
:param csr: A Certificate Signing Request
:param validity: Will be ignored for now
:param kwargs: Will be ignored for now
:return: Signed certificate
:raises Exception: if certificate signing fails
"""
LOG.debug("Signing a certificate request using Anchor")
try:
LOG.debug('Certificate: %s', csr)
r = requests.post(CONF.anchor.url, data={
'user': CONF.anchor.username,
'secret': CONF.anchor.password,
'encoding': 'pem',
'csr': csr})
if r.status_code != 200:
LOG.debug('Anchor returned: %s', r.content)
raise AnchorException("Anchor returned Status Code : "
+ str(r.status_code))
return r.content
except Exception as e:
LOG.error(_LE("Unable to sign certificate."))
raise exceptions.CertificateGenerationException(msg=e)

View File

@ -177,7 +177,10 @@ class LocalCertGenerator(cert_gen.CertGenerator):
csr = x509.CertificateSigningRequestBuilder().subject_name(
x509.Name([
x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, cn),
])).sign(pk, hashes.SHA256(), backends.default_backend())
])).add_extension(
x509.BasicConstraints(
ca=False, path_length=None), critical=True,
).sign(pk, hashes.SHA256(), backends.default_backend())
return csr.public_bytes(serialization.Encoding.PEM)
@classmethod

View File

@ -286,7 +286,19 @@ house_keeping_opts = [
default=10,
help=_('Number of threads performing amphora certificate'
' rotation'))
]
anchor_opts = [
cfg.StrOpt('url',
default='http://localhost:9999/v1/sign/default',
help=_('Anchor URL')),
cfg.StrOpt('username',
default='myusername',
help=_('Anchor username')),
cfg.StrOpt('password',
default='simplepassword',
help=_('Anchor password'),
secret=True)
]
# Register the configuration options
@ -299,6 +311,7 @@ cfg.CONF.register_opts(controller_worker_opts, group='controller_worker')
cfg.CONF.register_opts(task_flow_opts, group='task_flow')
cfg.CONF.register_opts(oslo_messaging_opts, group='oslo_messaging')
cfg.CONF.register_opts(house_keeping_opts, group='house_keeping')
cfg.CONF.register_opts(anchor_opts, group='anchor')
cfg.CONF.register_cli_opts(core_cli_opts)
cfg.CONF.register_opts(certificate_opts, group='certificates')
cfg.CONF.register_cli_opts(healthmanager_opts, group='health_manager')
@ -306,6 +319,7 @@ cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
cfg.CONF.register_opts(keystone_authtoken_v3_opts,
group='keystone_authtoken_v3')
# Ensure that the control exchange is set correctly
messaging.set_transport_defaults(control_exchange='octavia')
_SQL_CONNECTION_DEFAULT = 'sqlite://'

View File

@ -0,0 +1,115 @@
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cryptography.hazmat import backends
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography import x509
import mock
import octavia.tests.unit.base as base
class BaseLocalCSRTestCase(base.TestCase):
def setUp(self):
self.signing_digest = "sha256"
# Set up CSR data
csr_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=backends.default_backend()
)
csr = x509.CertificateSigningRequestBuilder().subject_name(
x509.Name([
x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, u"test"),
])).sign(csr_key, hashes.SHA256(), backends.default_backend())
self.certificate_signing_request = csr.public_bytes(
serialization.Encoding.PEM)
# Set up keys
self.ca_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=backends.default_backend()
)
self.ca_private_key_passphrase = b"Testing"
self.ca_private_key = self.ca_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.BestAvailableEncryption(
self.ca_private_key_passphrase),
)
super(BaseLocalCSRTestCase, self).setUp()
def test_generate_csr(self):
cn = 'test_cn'
# Attempt to generate a CSR
csr = self.cert_generator._generate_csr(
cn=cn,
private_key=self.ca_private_key,
passphrase=self.ca_private_key_passphrase
)
# Attempt to load the generated CSR
csro = x509.load_pem_x509_csr(data=csr,
backend=backends.default_backend())
# Make sure the CN is correct
self.assertEqual(cn, csro.subject.get_attributes_for_oid(
x509.oid.NameOID.COMMON_NAME)[0].value)
def test_generate_private_key(self):
bit_length = 1024
# Attempt to generate a private key
pk = self.cert_generator._generate_private_key(
bit_length=bit_length
)
# Attempt to load the generated private key
pko = serialization.load_pem_private_key(
data=pk, password=None, backend=backends.default_backend())
# Make sure the bit_length is what we set
self.assertEqual(pko.key_size, bit_length)
def test_generate_private_key_with_passphrase(self):
bit_length = 2048
# Attempt to generate a private key
pk = self.cert_generator._generate_private_key(
bit_length=bit_length,
passphrase=self.ca_private_key_passphrase
)
# Attempt to load the generated private key
pko = serialization.load_pem_private_key(
data=pk, password=self.ca_private_key_passphrase,
backend=backends.default_backend())
# Make sure the bit_length is what we set
self.assertEqual(pko.key_size, bit_length)
def test_generate_cert_key_pair_mock(self):
cn = 'test_cn'
with mock.patch.object(self.cert_generator, 'sign_cert') as m:
# Attempt to generate a cert/key pair
self.cert_generator.generate_cert_key_pair(
cn=cn,
validity=2 * 365 * 24 * 60 * 60,
)
self.assertTrue(m.called)

View File

@ -0,0 +1,48 @@
# Copyright 2015 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
import requests_mock
import six
from octavia.certificates.generator import anchor
from octavia.common import exceptions
from octavia.tests.unit.certificates.generator import local_csr
CONF = cfg.CONF
CONF.import_group('anchor', 'octavia.common.config')
class TestAnchorGenerator(local_csr.BaseLocalCSRTestCase):
def setUp(self):
super(TestAnchorGenerator, self).setUp()
self.cert_generator = anchor.AnchorCertGenerator
@requests_mock.mock()
def test_sign_cert(self, m):
m.post(CONF.anchor.url, content=six.b('test'))
# Attempt to sign a cert
signed_cert = self.cert_generator.sign_cert(
csr=self.certificate_signing_request
)
self.assertEqual("test", signed_cert.decode('ascii'))
self.assertTrue(m.called)
m.post(CONF.anchor.url, status_code=400)
self.assertRaises(exceptions.CertificateGenerationException,
self.cert_generator.sign_cert,
self.certificate_signing_request)

View File

@ -15,46 +15,20 @@ import datetime
from cryptography import exceptions as crypto_exceptions
from cryptography.hazmat import backends
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography import x509
import octavia.certificates.generator.local as local_cert_gen
import octavia.tests.unit.base as base
from octavia.tests.unit.certificates.generator import local_csr
class TestLocalGenerator(base.TestCase):
class TestLocalGenerator(local_csr.BaseLocalCSRTestCase):
def setUp(self):
super(TestLocalGenerator, self).setUp()
self.signing_digest = "sha256"
# Set up CSR data
csr_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=backends.default_backend()
)
csr = x509.CertificateSigningRequestBuilder().subject_name(
x509.Name([
x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, u"test"),
])).sign(csr_key, hashes.SHA256(), backends.default_backend())
self.certificate_signing_request = csr.public_bytes(
serialization.Encoding.PEM)
# Set up CA data
ca_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=backends.default_backend()
)
self.ca_private_key_passphrase = b"Testing"
self.ca_private_key = ca_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.BestAvailableEncryption(
self.ca_private_key_passphrase),
)
# Setup CA data
ca_cert = x509.CertificateBuilder()
valid_from_datetime = datetime.datetime.utcnow()
@ -75,19 +49,19 @@ class TestLocalGenerator(base.TestCase):
])
ca_cert = ca_cert.subject_name(subject_name)
ca_cert = ca_cert.issuer_name(subject_name)
ca_cert = ca_cert.public_key(ca_key.public_key())
signed_cert = ca_cert.sign(private_key=ca_key,
ca_cert = ca_cert.public_key(self.ca_key.public_key())
signed_cert = ca_cert.sign(private_key=self.ca_key,
algorithm=hashes.SHA256(),
backend=backends.default_backend())
self.ca_certificate = signed_cert.public_bytes(
encoding=serialization.Encoding.PEM)
super(TestLocalGenerator, self).setUp()
self.cert_generator = local_cert_gen.LocalCertGenerator
def test_sign_cert(self):
# Attempt sign a cert
signed_cert = local_cert_gen.LocalCertGenerator.sign_cert(
signed_cert = self.cert_generator.sign_cert(
csr=self.certificate_signing_request,
validity=2 * 365 * 24 * 60 * 60,
ca_cert=self.ca_certificate,
@ -128,7 +102,7 @@ class TestLocalGenerator(base.TestCase):
def test_sign_cert_invalid_algorithm(self):
self.assertRaises(
crypto_exceptions.UnsupportedAlgorithm,
local_cert_gen.LocalCertGenerator.sign_cert,
self.cert_generator.sign_cert,
csr=self.certificate_signing_request,
validity=2 * 365 * 24 * 60 * 60,
ca_cert=self.ca_certificate,
@ -137,58 +111,12 @@ class TestLocalGenerator(base.TestCase):
ca_digest='not_an_algorithm'
)
def test_generate_private_key(self):
bit_length = 1024
# Attempt to generate a private key
pk = local_cert_gen.LocalCertGenerator._generate_private_key(
bit_length=bit_length
)
# Attempt to load the generated private key
pko = serialization.load_pem_private_key(
data=pk, password=None, backend=backends.default_backend())
# Make sure the bit_length is what we set
self.assertEqual(pko.key_size, bit_length)
def test_generate_private_key_with_passphrase(self):
bit_length = 2048
# Attempt to generate a private key
pk = local_cert_gen.LocalCertGenerator._generate_private_key(
bit_length=bit_length,
passphrase=self.ca_private_key_passphrase
)
# Attempt to load the generated private key
pko = serialization.load_pem_private_key(
data=pk, password=self.ca_private_key_passphrase,
backend=backends.default_backend())
# Make sure the bit_length is what we set
self.assertEqual(pko.key_size, bit_length)
def test_generate_csr(self):
cn = 'test_cn'
# Attempt to generate a CSR
csr = local_cert_gen.LocalCertGenerator._generate_csr(
cn=cn,
private_key=self.ca_private_key,
passphrase=self.ca_private_key_passphrase
)
# Attempt to load the generated CSR
csro = x509.load_pem_x509_csr(data=csr,
backend=backends.default_backend())
# Make sure the CN is correct
self.assertEqual(cn, csro.subject.get_attributes_for_oid(
x509.oid.NameOID.COMMON_NAME)[0].value)
def test_generate_cert_key_pair(self):
cn = 'test_cn'
bit_length = 512
# Attempt to generate a cert/key pair
cert_object = local_cert_gen.LocalCertGenerator.generate_cert_key_pair(
cert_object = self.cert_generator.generate_cert_key_pair(
cn=cn,
validity=2 * 365 * 24 * 60 * 60,
bit_length=bit_length,
@ -207,4 +135,4 @@ class TestLocalGenerator(base.TestCase):
data=cert_object.private_key,
password=cert_object.private_key_passphrase,
backend=backends.default_backend())
self.assertIsNotNone(key)
self.assertIsNotNone(key)

View File

@ -59,6 +59,7 @@ octavia.network.drivers =
octavia.cert_generator =
local_cert_generator = octavia.certificates.generator.local:LocalCertGenerator
barbican_cert_generator = octavia.certificates.generator.barbican:BarbicanCertGenerator
anchor_cert_generator = octavia.certificates.generator.anchor:AnchorCertGenerator
octavia.cert_manager =
local_cert_manager = octavia.certificates.manager.local:LocalCertManager
barbican_cert_manager = octavia.certificates.manager.barbican:BarbicanCertManager