Add auto-unlock option

The auto-unlock option intialises vault and stores the keys and
root token in the leadership database. This option should
only be used in testing as it is probably undesirable to
store the vault keys in the leader db from a security pov.

Change-Id: I10ec2f009920acf47f5353f6f947520514350bc0
This commit is contained in:
Liam Young 2018-04-20 10:54:59 +00:00
parent 5d16f8011d
commit cb5c24366c
6 changed files with 380 additions and 99 deletions

View File

@ -58,3 +58,9 @@ options:
description: |
DNS record to use for DNS HA with MAAS. Do not use vip setting
if this is set.
auto-unlock:
type: boolean
default: false
description: >-
FOR TESTING ONLY. Initialise vault after deployment and store the keys
locally.

View File

@ -1,7 +1,12 @@
import functools
import json
import requests
import hvac
import tenacity
import charmhelpers.core.hookenv as hookenv
import charmhelpers.core.host as host
import charms.reactive
CHARM_ACCESS_ROLE = 'local-charm-access'
@ -44,6 +49,8 @@ path "sys/mounts/" {
capabilities = ["list"]
}"""
VAULT_HEALTH_URL = '{vault_addr}/v1/sys/health'
def binding_address(binding):
try:
@ -94,13 +101,15 @@ def create_local_charm_access_role(client, policies):
return client.get_role_id(CHARM_ACCESS_ROLE)
def setup_charm_vault_access(token):
def setup_charm_vault_access(token=None):
"""Create policies and role. Grant role to charm.
:param token: Token to use to authenticate with vault
:type token: str
:returns: Id of created role
:rtype: str"""
if not token:
token = hookenv.leader_get('token')
vault_url = get_api_url()
client = hvac.Client(
url=vault_url,
@ -118,3 +127,104 @@ def get_local_charm_access_role_id():
:rtype: str
"""
return hookenv.leader_get(CHARM_ACCESS_ROLE_ID)
def get_client():
"""Provide a client for talking to the vault api
:returns: vault client
:rtype: hvac.Client
"""
return hvac.Client(url=get_api_url())
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=10),
stop=tenacity.stop_after_attempt(10),
reraise=True)
def get_vault_health():
"""Query vault to retrieve health
:returns: Vault health
:rtype: dict
"""
response = requests.get(
VAULT_HEALTH_URL.format(vault_addr=get_api_url()))
return response.json()
def opportunistic_restart():
"""Restart vault if possible"""
if can_restart():
hookenv.log("Restarting vault", level=hookenv.DEBUG)
host.service_restart('vault')
else:
hookenv.log("Starting vault", level=hookenv.DEBUG)
host.service_start('vault')
def prepare_vault():
"""Setup vault as much as possible
Attempt to prepare vault for operation. Where possible, initialise, unseal
and create role for local charm access to vault.
"""
if not host.service_running('vault'):
hookenv.log("Defering unlock vault not running ", level=hookenv.DEBUG)
return
vault_health = get_vault_health()
if not vault_health['initialized'] and hookenv.is_leader():
initialize_vault()
if vault_health['sealed']:
unseal_vault()
if hookenv.is_leader():
setup_charm_vault_access()
def initialize_vault(shares=1, threshold=1):
"""Initialise vault
Initialise vault and store the resulting key(s) and token in the leader db.
:param shares: Number of shares to create
:type shares: int
:param threshold: Minimum number of shares needed to unlock
:type threshold: int
"""
client = get_client()
result = client.initialize(shares, threshold)
client.token = result['root_token']
hookenv.leader_set(
root_token=result['root_token'],
keys=json.dumps(result['keys']))
def unseal_vault(keys=None):
"""Unseal vault with provided keys. If no keys are provided retrieve from
leader db"""
client = get_client()
if not keys:
keys = json.loads(hookenv.leader_get()['keys'])
for key in keys:
client.unseal(key)
def can_restart():
"""Check if vault can be restarted
:returns: Can vault be restarted
:rtype: bool
"""
safe_restart = False
if not host.service_running('vault'):
safe_restart = True
elif hookenv.config('auto-unlock'):
safe_restart = True
else:
client = get_client()
if not client.is_initialized():
safe_restart = True
elif client.is_sealed():
safe_restart = True
hookenv.log(
"Safe to restart: {}".format(safe_restart),
level=hookenv.DEBUG)
return safe_restart

View File

@ -1,9 +1,6 @@
import base64
import hvac
import psycopg2
import requests
import subprocess
import tenacity
from charmhelpers.contrib.charmsupport.nrpe import (
@ -28,9 +25,9 @@ from charmhelpers.core.hookenv import (
)
from charmhelpers.core.host import (
service,
service_restart,
service_running,
service_start,
write_file,
)
@ -44,6 +41,7 @@ from charms.reactive import (
remove_state,
set_state,
when,
when_file_changed,
when_not,
)
@ -76,7 +74,6 @@ VAULT_INDEX_DDL = """
CREATE INDEX IF NOT EXISTS parent_path_idx ON vault_kv_store (parent_path);
"""
VAULT_HEALTH_URL = '{vault_addr}/v1/sys/health'
OPTIONAL_INTERFACES = [
['etcd'],
@ -85,32 +82,8 @@ REQUIRED_INTERFACES = [
['shared-db', 'db.master']
]
def get_client():
return hvac.Client(url=vault.get_api_url())
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=10),
stop=tenacity.stop_after_attempt(10),
reraise=True)
def get_vault_health():
response = requests.get(
VAULT_HEALTH_URL.format(vault_addr=vault.get_api_url()))
return response.json()
def can_restart():
safe_restart = False
if not service_running('vault'):
safe_restart = True
else:
client = get_client()
if not client.is_initialized():
safe_restart = True
elif client.is_sealed():
safe_restart = True
log("Safe to restart: {}".format(safe_restart), level=DEBUG)
return safe_restart
VAULT_CONFIG = '/var/snap/vault/common/vault.hcl'
VAULT_SYSTEMD_CONFIG = '/etc/systemd/system/vault.service'
def ssl_available(config):
@ -158,9 +131,11 @@ def snap_refresh():
if validate_snap_channel(channel):
clear_flag('snap.channel.invalid')
snap.refresh('vault', channel=channel)
if can_restart():
if vault.can_restart():
log("Restarting vault", level=DEBUG)
service_restart('vault')
if config('auto-unlock'):
vault.prepare_vault()
else:
set_flag('snap.channel.invalid')
@ -192,20 +167,16 @@ def configure_vault(context):
log("Rendering vault.hcl.j2", level=DEBUG)
render(
'vault.hcl.j2',
'/var/snap/vault/common/vault.hcl',
VAULT_CONFIG,
context,
perms=0o600)
log("Rendering vault systemd configuation", level=DEBUG)
render(
'vault.service.j2',
'/etc/systemd/system/vault.service',
VAULT_SYSTEMD_CONFIG,
{},
perms=0o644)
if can_restart():
log("Restarting vault", level=DEBUG)
service_restart('vault')
else:
service_start('vault') # restart seals the vault
service('enable', 'vault')
log("Opening vault port", level=DEBUG)
open_port(8200)
@ -409,6 +380,14 @@ def cluster_connected(hacluster):
hacluster.bind_resources()
@when_file_changed(VAULT_CONFIG, VAULT_SYSTEMD_CONFIG)
def file_change_auto_unlock_mode():
log("Calling opportunistic_restart", level=DEBUG)
vault.opportunistic_restart()
if config('auto-unlock'):
vault.prepare_vault()
@when('snap.installed.vault')
def prime_assess_status():
atexit(_assess_status)
@ -511,7 +490,7 @@ def _assess_status():
health = None
if service_running('vault'):
health = get_vault_health()
health = vault.get_vault_health()
application_version_set(health.get('version'))
_missing_interfaces = []

View File

@ -12,3 +12,13 @@ smoke_bundles:
- xenial-mysql
dev_bundles:
- bionic
target_deploy_status:
vault:
workload-status: blocked
workload-status-message: Vault needs to be initialized
easyrsa:
workload-status-message: Certificate Authority connected.
etcd:
workload-status-message: Healthy
postgresql:
workload-status-message: Live

View File

@ -7,6 +7,16 @@ import unit_tests.test_utils
class TestLibCharmVault(unit_tests.test_utils.CharmTestCase):
_health_response = {
"initialized": True,
"sealed": False,
"standby": False,
"server_time_utc": 1523952750,
"version": "0.9.0",
"cluster_name": "vault-cluster-9dd8dd12",
"cluster_id": "1ea3d74c-3819-fbaf-f780-bae0babc998f"
}
def setUp(self):
super(TestLibCharmVault, self).setUp()
self.obj = vault
@ -97,3 +107,211 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase):
network_get_primary_address.return_value = '1.2.3.4'
self.assertEqual(vault.get_cluster_url(), 'http://1.2.3.4:8201')
network_get_primary_address.assert_called_with('cluster')
@patch.object(vault.hvac, 'Client')
@patch.object(vault, 'get_api_url')
def test_get_client(self, get_api_url, hvac_Client):
get_api_url.return_value = 'http://this-unit'
vault.get_client()
hvac_Client.assert_called_once_with(url='http://this-unit')
@patch.object(vault.host, 'service_running')
def test_can_restart_vault_down(self, service_running):
service_running.return_value = False
self.assertTrue(vault.can_restart())
@patch.object(vault.host, 'service_running')
@patch.object(vault.hookenv, 'config')
@patch.object(vault, 'get_client')
def test_can_restart_not_initialized(self, get_client, config,
service_running):
config.return_value = False
service_running.return_value = True
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = False
get_client.return_value = hvac_mock
self.assertTrue(vault.can_restart())
hvac_mock.is_initialized.assert_called_once_with()
@patch.object(vault.host, 'service_running')
@patch.object(vault.hookenv, 'config')
@patch.object(vault, 'get_client')
def test_can_restart_sealed(self, get_client, config, service_running):
config.return_value = False
service_running.return_value = True
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = True
hvac_mock.is_sealed.return_value = True
get_client.return_value = hvac_mock
self.assertTrue(vault.can_restart())
hvac_mock.is_initialized.assert_called_once_with()
hvac_mock.is_sealed.assert_called_once_with()
@patch.object(vault.host, 'service_running')
@patch.object(vault.hookenv, 'config')
@patch.object(vault, 'get_client')
def test_can_restart_unsealed(self, get_client, config, service_running):
config.return_value = False
service_running.return_value = True
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = True
hvac_mock.is_sealed.return_value = False
get_client.return_value = hvac_mock
self.assertFalse(vault.can_restart())
@patch.object(vault.host, 'service_running')
@patch.object(vault.hookenv, 'config')
def test_can_restart_auto_unlock(self, config, service_running):
config.return_value = True
service_running.return_value = True
self.assertTrue(vault.can_restart())
@patch.object(vault, 'get_api_url')
@patch.object(vault, 'requests')
def test_get_vault_health(self, requests, get_api_url):
get_api_url.return_value = "https://vault.demo.com:8200"
mock_response = mock.MagicMock()
mock_response.json.return_value = self._health_response
requests.get.return_value = mock_response
self.assertEqual(vault.get_vault_health(),
self._health_response)
requests.get.assert_called_with(
"https://vault.demo.com:8200/v1/sys/health")
mock_response.json.assert_called_once()
@patch.object(vault, 'setup_charm_vault_access')
@patch.object(vault.hookenv, 'is_leader')
@patch.object(vault, 'unseal_vault')
@patch.object(vault, 'initialize_vault')
@patch.object(vault, 'get_vault_health')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_running')
def test_prepare_vault(self, service_running, log, get_vault_health,
initialize_vault, unseal_vault, is_leader,
setup_charm_vault_access):
is_leader.return_value = True
service_running.return_value = True
get_vault_health.return_value = {
'initialized': False,
'sealed': True}
vault.prepare_vault()
initialize_vault.assert_called_once_with()
setup_charm_vault_access.assert_called_once_with()
unseal_vault.assert_called_once_with()
setup_charm_vault_access.assert_called_once_with()
@patch.object(vault.hookenv, 'is_leader')
@patch.object(vault, 'unseal_vault')
@patch.object(vault, 'initialize_vault')
@patch.object(vault, 'get_vault_health')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_running')
def test_prepare_vault_non_leader(self, service_running, log,
get_vault_health, initialize_vault,
unseal_vault, is_leader):
is_leader.return_value = False
service_running.return_value = True
get_vault_health.return_value = {
'initialized': False,
'sealed': True}
vault.prepare_vault()
self.assertFalse(initialize_vault.called)
unseal_vault.assert_called_once_with()
@patch.object(vault, 'unseal_vault')
@patch.object(vault, 'initialize_vault')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_running')
def test_prepare_vault_svc_down(self, service_running, log,
initialize_vault, unseal_vault):
service_running.return_value = False
vault.prepare_vault()
self.assertFalse(initialize_vault.called)
self.assertFalse(unseal_vault.called)
@patch.object(vault, 'setup_charm_vault_access')
@patch.object(vault.hookenv, 'is_leader')
@patch.object(vault, 'unseal_vault')
@patch.object(vault, 'initialize_vault')
@patch.object(vault, 'get_vault_health')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_running')
def test_prepare_vault_initialised(self, service_running, log,
get_vault_health, initialize_vault,
unseal_vault, is_leader,
setup_charm_vault_access):
is_leader.return_Value = False
service_running.return_value = True
get_vault_health.return_value = {
'initialized': True,
'sealed': True}
vault.prepare_vault()
self.assertFalse(initialize_vault.called)
unseal_vault.assert_called_once_with()
@patch.object(vault, 'setup_charm_vault_access')
@patch.object(vault.hookenv, 'is_leader')
@patch.object(vault, 'unseal_vault')
@patch.object(vault, 'initialize_vault')
@patch.object(vault, 'get_vault_health')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_running')
def test_prepare_vault_unsealed(self, service_running, log,
get_vault_health, initialize_vault,
unseal_vault, is_leader,
setup_charm_vault_access):
is_leader.return_Value = False
service_running.return_value = True
get_vault_health.return_value = {
'initialized': True,
'sealed': False}
vault.prepare_vault()
self.assertFalse(initialize_vault.called)
self.assertFalse(unseal_vault.called)
@patch.object(vault.hookenv, 'leader_set')
@patch.object(vault, 'get_client')
def test_initialize_vault(self, get_client, leader_set):
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = True
hvac_mock.initialize.return_value = {
'keys': ['c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e'],
'keys_base64': ['xX35oUPVVCNIO5B26nu6SbY65DK/dHKfd6+05y1Afcw='],
'root_token': 'dee94df7-23a3-9bf2-cb96-e943537c2b76'
}
get_client.return_value = hvac_mock
vault.initialize_vault()
hvac_mock.initialize.assert_called_once_with(1, 1)
leader_set.assert_called_once_with(
keys='["c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e"]',
root_token='dee94df7-23a3-9bf2-cb96-e943537c2b76')
@patch.object(vault.hookenv, 'leader_get')
@patch.object(vault, 'get_client')
def test_unseal_vault(self, get_client, leader_get):
hvac_mock = mock.MagicMock()
get_client.return_value = hvac_mock
leader_get.return_value = {
'root_token': 'dee94df7-23a3-9bf2-cb96-e943537c2b76',
'keys': '["c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e"]'
}
vault.unseal_vault()
hvac_mock.unseal.assert_called_once_with(
'c579a143d55423483b9076ea7bba49b63ae432bf74729f77afb4e')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_restart')
@patch.object(vault, 'can_restart')
def test_opportunistic_restart(self, can_restart, service_restart, log):
can_restart.return_value = True
vault.opportunistic_restart()
service_restart.assert_called_once_with('vault')
@patch.object(vault.hookenv, 'log')
@patch.object(vault.host, 'service_start')
@patch.object(vault, 'can_restart')
def test_opportunistic_restart_no_restart(self, can_restart, service_start,
log):
can_restart.return_value = False
vault.opportunistic_restart()
service_start.assert_called_once_with('vault')

View File

@ -58,7 +58,6 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'open_port',
'service_restart',
'service_running',
'service_start',
'set_state',
'status_set',
'remove_state',
@ -86,7 +85,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'ssl-cert': 'acert',
'ssl-key': 'akey'}))
@patch.object(handlers, 'can_restart')
@patch.object(handlers.vault, 'can_restart')
def test_configure_vault(self, can_restart):
can_restart.return_value = True
self.config.return_value = {'disable-mlock': False}
@ -215,7 +214,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
@patch.object(handlers, 'save_etcd_client_credentials')
@patch.object(handlers.vault, 'get_cluster_url')
@patch.object(handlers, 'can_restart')
@patch.object(handlers.vault, 'can_restart')
@patch.object(handlers.vault, 'get_api_url')
def test_configure_vault_etcd(self, get_api_url, can_restart,
get_cluster_url,
@ -260,55 +259,8 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
)
self.is_flag_set.assert_called_with('etcd.tls.available')
@patch.object(handlers.hvac, 'Client')
@patch.object(handlers.vault, 'get_api_url')
def test_get_client(self, get_api_url, hvac_Client):
get_api_url.return_value = 'http://this-unit'
handlers.get_client()
hvac_Client.assert_called_once_with(url='http://this-unit')
def test_can_restart_vault_down(self):
self.service_running.return_value = False
self.assertTrue(handlers.can_restart())
@patch.object(handlers, 'get_client')
def test_can_restart_not_initialized(self, get_client):
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = False
get_client.return_value = hvac_mock
self.assertTrue(handlers.can_restart())
@patch.object(handlers, 'get_client')
def test_can_restart_sealed(self, get_client):
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = True
hvac_mock.is_sealed.return_value = True
get_client.return_value = hvac_mock
self.assertTrue(handlers.can_restart())
@patch.object(handlers, 'get_client')
def test_can_restart_unsealed(self, get_client):
hvac_mock = mock.MagicMock()
hvac_mock.is_initialized.return_value = True
hvac_mock.is_sealed.return_value = False
get_client.return_value = hvac_mock
self.assertFalse(handlers.can_restart())
@patch.object(handlers.vault, 'get_api_url')
@patch.object(handlers, 'requests')
def test_get_vault_health(self, requests, get_api_url):
get_api_url.return_value = "https://vault.demo.com:8200"
mock_response = mock.MagicMock()
mock_response.json.return_value = self._health_response
requests.get.return_value = mock_response
self.assertEqual(handlers.get_vault_health(),
self._health_response)
requests.get.assert_called_with(
"https://vault.demo.com:8200/v1/sys/health")
mock_response.json.assert_called_once()
@patch.object(handlers, '_assess_interface_groups')
@patch.object(handlers, 'get_vault_health')
@patch.object(handlers.vault, 'get_vault_health')
def test_assess_status(self, get_vault_health,
_assess_interface_groups):
self.is_flag_set.return_value = False
@ -356,7 +308,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
self.is_flag_set.assert_called_with('config.dns_vip.invalid')
@patch.object(handlers, '_assess_interface_groups')
@patch.object(handlers, 'get_vault_health')
@patch.object(handlers.vault, 'get_vault_health')
def test_assess_status_not_running(self, get_vault_health,
_assess_interface_groups):
self.is_flag_set.return_value = False
@ -368,7 +320,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'blocked', 'Vault service not running')
@patch.object(handlers, '_assess_interface_groups')
@patch.object(handlers, 'get_vault_health')
@patch.object(handlers.vault, 'get_vault_health')
def test_assess_status_vault_init(self, get_vault_health,
_assess_interface_groups):
self.is_flag_set.return_value = False
@ -380,7 +332,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'blocked', 'Vault needs to be initialized')
@patch.object(handlers, '_assess_interface_groups')
@patch.object(handlers, 'get_vault_health')
@patch.object(handlers.vault, 'get_vault_health')
def test_assess_status_vault_sealed(self, get_vault_health,
_assess_interface_groups):
self.is_flag_set.return_value = False
@ -438,17 +390,23 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
self.config.assert_called_with('channel')
self.set_flag.assert_called_with('snap.channel.invalid')
@patch.object(handlers, 'can_restart')
@patch.object(handlers.vault, 'can_restart')
def test_snap_refresh_restartable(self, can_restart):
self.config.return_value = 'edge'
conf = {
'channel': 'edge',
'auto-unlock': False}
self.config.side_effect = lambda x: conf[x]
can_restart.return_value = True
handlers.snap_refresh()
self.snap.refresh.assert_called_with('vault', channel='edge')
self.config.assert_called_with('channel')
self.service_restart.assert_called_with('vault')
self.clear_flag.assert_called_with('snap.channel.invalid')
config_calls = [
mock.call('channel'),
mock.call('auto-unlock')]
self.config.assert_has_calls(config_calls)
@patch.object(handlers, 'can_restart')
@patch.object(handlers.vault, 'can_restart')
def test_snap_refresh_not_restartable(self, can_restart):
self.config.return_value = 'edge'
can_restart.return_value = False