Make cert and ca cert ttl configurable

Currently the default ttl for a cert is not configurable
and defaults to one month. This patch makes the ttl
configurable and updates the default for new and
re-generated certs to 1 year and 10 years for the CA
cert.

Closes-Bug: #1841138

Change-Id: Iaa6709c74d64c4191b44b92d4cfb3a3dbbb3fdc8
This commit is contained in:
Seyeong Kim 2019-08-23 15:30:53 +09:00 committed by Rodrigo Barbieri
parent 77033874d1
commit 5f233696a4
6 changed files with 211 additions and 74 deletions

View File

@ -53,7 +53,7 @@ upload-signed-csr:
Specifies if clients can request any CN
max-ttl:
type: string
default: '87598h'
default: '8760h'
description: >-
Specifies the maximum Time To Live
root-ca:
@ -70,7 +70,7 @@ generate-root-ca:
properties:
ttl:
type: string
default: '87598h'
default: '87599h'
description: >-
Specifies the Time To Live for the root CA certificate
allow-any-name:
@ -113,7 +113,7 @@ generate-root-ca:
for CNs, DNS SANs, and the host part of email addresses.
max-ttl:
type: string
default: '696h'
default: '8760h'
description: >-
Specifies the maximum Time To Live for generated certificates.
get-root-ca:

View File

@ -84,3 +84,19 @@ options:
Hostname to be used for the API URL. This hostname should exist as a DNS
record and be resolvable by the charms that will consume the relation
with vault.
max-ttl:
type: string
default: '87600h'
description: >-
Max allowed TTL to use when generating certs (must be greater than the
default).
default-ca-ttl:
type: string
default: '87599h'
description: >-
Default TTL to use when generating CA certs.
default-ttl:
type: string
default: '8759h'
description: >-
Default TTL to use when generating certs.

View File

@ -10,7 +10,7 @@ CHARM_PKI_ROLE = "local"
CHARM_PKI_ROLE_CLIENT = "local-client"
def configure_pki_backend(client, name, ttl=None):
def configure_pki_backend(client, name, ttl=None, max_ttl=None):
"""Ensure a pki backend is enabled
:param client: Vault client
@ -26,7 +26,9 @@ def configure_pki_backend(client, name, ttl=None):
description='Charm created PKI backend',
mount_point=name,
# Default ttl to 10 years
config={'max_lease_ttl': ttl or '87600h'})
config={
'default_lease_ttl': ttl or '8759h',
'max_lease_ttl': max_ttl or '87600h'})
def disable_pki_backend():
@ -42,7 +44,7 @@ def disable_pki_backend():
client.disable_secret_backend(CHARM_PKI_MP)
def tune_pki_backend(ttl=None):
def tune_pki_backend(ttl=None, max_ttl=None):
"""Assert tuning options for Charm PKI backend
:param ttl: TTL
@ -53,7 +55,8 @@ def tune_pki_backend(ttl=None):
client.tune_secret_backend(
backend_type='pki',
mount_point=CHARM_PKI_MP,
max_lease_ttl=ttl or '87600h')
default_lease_ttl=ttl or '8759h',
max_lease_ttl=max_ttl or '87600h')
def is_ca_ready(client, name, role):
@ -86,7 +89,7 @@ def get_ca():
return hookenv.leader_get('root-ca')
def generate_certificate(cert_type, common_name, sans):
def generate_certificate(cert_type, common_name, sans, ttl, max_ttl):
"""
Create a certificate and key for the given CN and SANs, if requested.
@ -99,7 +102,7 @@ def generate_certificate(cert_type, common_name, sans):
:rtype: tuple
"""
client = vault.get_local_client()
configure_pki_backend(client, CHARM_PKI_MP)
configure_pki_backend(client, CHARM_PKI_MP, ttl, max_ttl)
if not is_ca_ready(client, CHARM_PKI_MP, CHARM_PKI_ROLE):
raise vault.VaultNotReady("CA not ready")
role = None
@ -208,25 +211,13 @@ def upload_signed_csr(pem, allowed_domains, allow_subdomains=True,
# Configure a role which maps to a policy for accessing this pki
if not max_ttl:
max_ttl = '87598h'
client.write(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE),
allowed_domains=allowed_domains,
allow_subdomains=allow_subdomains,
enforce_hostnames=enforce_hostnames,
allow_any_name=allow_any_name,
max_ttl=max_ttl,
server_flag=True,
client_flag=True) # server certs can also be used as client certs
# Configure a role for using this PKI to issue server certs
client.write(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE_CLIENT),
allowed_domains=allowed_domains,
allow_subdomains=allow_subdomains,
enforce_hostnames=enforce_hostnames,
allow_any_name=allow_any_name,
max_ttl=max_ttl,
server_flag=False, # client certs cannot be used as server certs
client_flag=True)
write_roles(client,
allow_any_name=allow_any_name,
allowed_domains=allowed_domains,
allow_subdomains=allow_subdomains,
enforce_hostnames=enforce_hostnames,
max_ttl=max_ttl,
client_flag=True)
def generate_root_ca(ttl='87599h', allow_any_name=True, allowed_domains=None,
@ -284,30 +275,16 @@ def generate_root_ca(ttl='87599h', allow_any_name=True, allowed_domains=None,
issuing_certificates="{}/v1/{}/ca".format(addr, CHARM_PKI_MP),
crl_distribution_points="{}/v1/{}/crl".format(addr, CHARM_PKI_MP)
)
# Configure a role for using this PKI to issue server certs
client.write(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE),
allow_any_name=allow_any_name,
allowed_domains=allowed_domains,
allow_bare_domains=allow_bare_domains,
allow_subdomains=allow_subdomains,
allow_glob_domains=allow_glob_domains,
enforce_hostnames=enforce_hostnames,
max_ttl=max_ttl,
server_flag=True,
client_flag=True) # server certs can also be used as client certs
# Configure a role for using this PKI to issue client-only certs
client.write(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE_CLIENT),
allow_any_name=allow_any_name,
allowed_domains=allowed_domains,
allow_bare_domains=allow_bare_domains,
allow_subdomains=allow_subdomains,
allow_glob_domains=allow_glob_domains,
enforce_hostnames=enforce_hostnames,
max_ttl=max_ttl,
server_flag=False, # client certs cannot be used as server certs
client_flag=True)
write_roles(client,
allow_any_name=allow_any_name,
allowed_domains=allowed_domains,
allow_bare_domains=allow_bare_domains,
allow_subdomains=allow_subdomains,
allow_glob_domains=allow_glob_domains,
enforce_hostnames=enforce_hostnames,
max_ttl=max_ttl,
client_flag=True)
return cert
@ -323,3 +300,33 @@ def sort_sans(sans):
ip_sans = {s for s in sans if ch_ip.is_ip(s)}
alt_names = set(sans).difference(ip_sans)
return sorted(list(ip_sans)), sorted(list(alt_names))
def write_roles(client, **kwargs):
# Configure a role for using this PKI to issue server certs
client.write(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE),
server_flag=True,
**kwargs)
# Configure a role for using this PKI to issue client-only certs
client.write(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE_CLIENT),
server_flag=False, # client certs cannot be used as server certs
**kwargs)
def update_roles(**kwargs):
client = vault.get_local_client()
# local and local-client contain the same data except for server_flag,
# so we only need to read one, but update both
local = client.read(
'{}/roles/{}'.format(CHARM_PKI_MP, CHARM_PKI_ROLE))['data']
# the reason we handle as kwargs here is because updating n-1 fields
# causes all the others to reset. Therefore we always need to read what
# the current values of all fields are, and apply all of them as well
# so they are not reset. In case of new fields are added in the future,
# this code makes sure that they are not reset automatically (if set
# somewhere else in code) when this function is invoked.
local.update(**kwargs)
del local['server_flag']
write_roles(client, **local)

View File

@ -703,17 +703,19 @@ def _assess_status():
def auto_generate_root_ca_cert():
actions_yaml = yaml.load(Path('actions.yaml').read_text())
props = actions_yaml['generate-root-ca']['properties']
ttl = config()['default-ca-ttl']
max_ttl = config()['max-ttl']
action_config = {key: value['default'] for key, value in props.items()}
try:
root_ca = vault_pki.generate_root_ca(
ttl=action_config['ttl'],
allow_any_name=action_config['allow-any-name'],
allowed_domains=action_config['allowed-domains'],
allow_bare_domains=action_config['allow-bare-domains'],
allow_subdomains=action_config['allow-subdomains'],
allow_glob_domains=action_config['allow-glob-domains'],
enforce_hostnames=action_config['enforce-hostnames'],
max_ttl=action_config['max-ttl'])
ttl=ttl,
max_ttl=max_ttl)
leader_set({'root-ca': root_ca})
set_flag('charm.vault.ca.ready')
set_flag('charm.vault.ca.auto-generated')
@ -756,9 +758,11 @@ def publish_global_client_cert():
reissue_requested = is_flag_set('certificates.reissue.global.requested')
tls = endpoint_from_flag('certificates.available')
if not cert_created or reissue_requested:
ttl = config()['default-ttl']
max_ttl = config()['max-ttl']
bundle = vault_pki.generate_certificate('client',
'global-client',
[])
[], ttl, max_ttl)
unitdata.kv().set('charm.vault.global-client-cert', bundle)
set_flag('charm.vault.global-client-cert.created')
clear_flag('certificates.reissue.global.requested')
@ -782,9 +786,11 @@ def create_certs():
log('Processing certificate request from {} for {}'.format(
request.unit_name, request.common_name))
try:
ttl = config()['default-ttl']
max_ttl = config()['max-ttl']
bundle = vault_pki.generate_certificate(request.cert_type,
request.common_name,
request.sans)
request.sans, ttl, max_ttl)
request.set_cert(bundle['certificate'], bundle['private_key'])
except vault.VaultInvalidRequest as e:
log(str(e), level=ERROR)
@ -814,5 +820,18 @@ def post_series_upgrade():
def tune_pki_backend():
"""Ensure Vault PKI backend is correctly tuned
"""
vault_pki.tune_pki_backend()
ttl = config()['default-ttl']
max_ttl = config()['max-ttl']
vault_pki.tune_pki_backend(ttl=ttl, max_ttl=max_ttl)
set_flag('pki.backend.tuned')
@when('leadership.is_leader',
'charm.vault.ca.ready')
@when('config.set.default-ttl')
@when('config.set.max-ttl')
def tune_pki_backend_config_changed():
ttl = config()['default-ttl']
max_ttl = config()['max-ttl']
vault_pki.tune_pki_backend(ttl=ttl, max_ttl=max_ttl)
vault_pki.update_roles(max_ttl=max_ttl)

View File

@ -22,10 +22,12 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
vault_pki.configure_pki_backend(
client_mock,
'my_backend',
ttl=42)
ttl=42, max_ttl=42)
client_mock.enable_secret_backend.assert_called_once_with(
backend_type='pki',
config={'max_lease_ttl': 42},
config={
'default_lease_ttl': 42,
'max_lease_ttl': 42},
description='Charm created PKI backend',
mount_point='my_backend')
@ -38,7 +40,9 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
'my_backend')
client_mock.enable_secret_backend.assert_called_once_with(
backend_type='pki',
config={'max_lease_ttl': '87600h'},
config={
'default_lease_ttl': '8759h',
'max_lease_ttl': '87600h'},
description='Charm created PKI backend',
mount_point='my_backend')
@ -121,13 +125,16 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
]
vault_pki.generate_certificate('server',
'example.com',
([], []))
([], []),
ttl='3456h', max_ttl='3456h')
vault_pki.generate_certificate('server',
'example.com',
(['ip1'], ['alt1']))
(['ip1'], ['alt1']),
ttl='3456h', max_ttl='3456h')
vault_pki.generate_certificate('client',
'example.com',
(['ip1', 'ip2'], ['alt1', 'alt2']))
(['ip1', 'ip2'], ['alt1', 'alt2']),
ttl='3456h', max_ttl='3456h')
client_mock.write.assert_has_calls(write_calls)
@patch.object(vault_pki, 'is_ca_ready')
@ -140,7 +147,8 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
get_local_client.return_value = client_mock
is_ca_ready.return_value = False
with self.assertRaises(vault_pki.vault.VaultNotReady):
vault_pki.generate_certificate('server', 'exmaple.com', [])
vault_pki.generate_certificate('server', 'exmaple.com', [],
ttl='3456h', max_ttl='3456h')
@patch.object(vault_pki, 'is_ca_ready')
@patch.object(vault_pki, 'configure_pki_backend')
@ -152,7 +160,8 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
get_local_client.return_value = client_mock
is_ca_ready.return_value = True
with self.assertRaises(vault_pki.vault.VaultInvalidRequest):
vault_pki.generate_certificate('unknown', 'exmaple.com', [])
vault_pki.generate_certificate('unknown', 'exmaple.com', [],
'3456h', '3456h')
@patch.object(vault_pki, 'is_ca_ready')
@patch.object(vault_pki, 'configure_pki_backend')
@ -165,7 +174,8 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
is_ca_ready.return_value = True
client_mock.write.side_effect = hvac.exceptions.InvalidRequest
with self.assertRaises(vault_pki.vault.VaultInvalidRequest):
vault_pki.generate_certificate('server', 'exmaple.com', [])
vault_pki.generate_certificate('server', 'exmaple.com', [],
ttl='3456h', max_ttl='3456h')
@patch.object(vault_pki, 'configure_pki_backend')
@patch.object(vault_pki.vault, 'get_local_client')
@ -373,11 +383,55 @@ class TestLibCharmVaultPKI(unit_tests.test_utils.CharmTestCase):
is_backend_mounted.return_value = True
mock_client = mock.MagicMock()
get_local_client.return_value = mock_client
vault_pki.tune_pki_backend(ttl='3456h')
vault_pki.tune_pki_backend(ttl='3456h', max_ttl='3456h')
is_backend_mounted.assert_called_with(mock_client,
vault_pki.CHARM_PKI_MP)
mock_client.tune_secret_backend.assert_called_with(
backend_type='pki',
mount_point=vault_pki.CHARM_PKI_MP,
max_lease_ttl='3456h'
max_lease_ttl='3456h',
default_lease_ttl='3456h'
)
@patch.object(vault_pki.vault, 'get_local_client')
def test_update_roles(self, get_local_client):
mock_client = mock.MagicMock()
get_local_client.return_value = mock_client
mock_client.read.return_value = {
'data': {
'allow_any_name': True,
'allowed_domains': 'domains',
'allow_bare_domains': True,
'allow_subdomains': True,
'allow_glob_domains': False,
'enforce_hostnames': True,
'max_ttl': '10h',
'server_flag': True,
'client_flag': True,
}
}
vault_pki.update_roles(max_ttl='20h')
mock_client.write.assert_has_calls([
mock.call('{}/roles/{}'.format(
vault_pki.CHARM_PKI_MP, vault_pki.CHARM_PKI_ROLE),
allow_any_name=True,
allowed_domains='domains',
allow_bare_domains=True,
allow_subdomains=True,
allow_glob_domains=False,
enforce_hostnames=True,
max_ttl='20h',
server_flag=True,
client_flag=True),
mock.call('{}/roles/{}'.format(
vault_pki.CHARM_PKI_MP, vault_pki.CHARM_PKI_ROLE_CLIENT),
allow_any_name=True,
allowed_domains='domains',
allow_bare_domains=True,
allow_subdomains=True,
allow_glob_domains=False,
enforce_hostnames=True,
max_ttl='20h',
server_flag=False,
client_flag=True),
])

View File

@ -747,7 +747,13 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
@mock.patch.object(handlers, 'vault_pki')
def test_publish_global_client_cert_reissue(self, vault_pki):
self.config.return_value = {
'default-ttl': '3456h',
'max-ttl': '3456h',
}
tls = self.endpoint_from_flag.return_value
self.is_flag_set.side_effect = [True, True]
bundle = {'certificate': 'crt',
'private_key': 'key'}
@ -755,7 +761,9 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
handlers.publish_global_client_cert()
vault_pki.generate_certificate.assert_called_with('client',
'global-client',
[])
[],
'3456h',
'3456h')
self.unitdata.kv().set.assert_called_with('charm.vault.'
'global-client-cert',
bundle)
@ -765,6 +773,11 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
@mock.patch.object(handlers, 'vault_pki')
def test_publish_global_client_certe(self, vault_pki):
self.config.return_value = {
'default-ttl': '3456h',
'max-ttl': '3456h',
}
tls = self.endpoint_from_flag.return_value
self.is_flag_set.side_effect = [False, False]
bundle = {'certificate': 'crt',
@ -773,7 +786,9 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
handlers.publish_global_client_cert()
vault_pki.generate_certificate.assert_called_with('client',
'global-client',
[])
[],
'3456h',
'3456h')
self.unitdata.kv().set.assert_called_with('charm.vault.'
'global-client-cert',
bundle)
@ -783,6 +798,11 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
@mock.patch.object(handlers, 'vault_pki')
def test_create_certs(self, vault_pki):
self.config.return_value = {
'default-ttl': '3456h',
'max-ttl': '3456h',
}
tls = self.endpoint_from_flag.return_value
self.is_flag_set.return_value = False
tls.new_requests = [mock.Mock(cert_type='cert_type1',
@ -801,9 +821,12 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
]
handlers.create_certs()
vault_pki.generate_certificate.assert_has_calls([
mock.call('cert_type1', 'common_name1', 'sans1'),
mock.call('invalid', 'invalid', 'invalid'),
mock.call('cert_type2', 'common_name2', 'sans2'),
mock.call('cert_type1', 'common_name1', 'sans1',
'3456h', '3456h'),
mock.call('invalid', 'invalid', 'invalid',
'3456h', '3456h'),
mock.call('cert_type2', 'common_name2', 'sans2',
'3456h', '3456h')
])
tls.new_requests[0].set_cert.assert_has_calls([
mock.call('crt1', 'key1'),
@ -815,10 +838,28 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
@mock.patch.object(handlers, 'vault_pki')
def test_tune_pki_backend(self, vault_pki):
self.config.return_value = {
'default-ttl': '8759h',
'max-ttl': '87600h',
}
handlers.tune_pki_backend()
vault_pki.tune_pki_backend.assert_called_once_with()
vault_pki.tune_pki_backend.assert_called_once_with(max_ttl='87600h',
ttl='8759h')
self.set_flag.assert_called_once_with('pki.backend.tuned')
@mock.patch.object(handlers, 'vault_pki')
def test_tune_pki_backend_config_changed(self, vault_pki):
self.config.return_value = {
'default-ttl': '8759h',
'max-ttl': '87600h',
}
handlers.tune_pki_backend_config_changed()
vault_pki.tune_pki_backend.assert_called_once_with(max_ttl='87600h',
ttl='8759h')
vault_pki.update_roles.assert_called_once_with(max_ttl='87600h')
@mock.patch.object(handlers, 'config')
@mock.patch.object(handlers, 'clear_flag')
@mock.patch.object(handlers, 'set_flag')