charm-vault/src/lib/charm/vault.py

305 lines
8.5 KiB
Python

# Copyright 2018 Canonical Ltd
#
# 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.
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'
CHARM_ACCESS_ROLE_ID = 'local-charm-access-id'
CHARM_POLICY_NAME = 'local-charm-policy'
CHARM_POLICY = """
# Allow managment of policies starting with charm- prefix
path "sys/policy/charm-*" {
capabilities = ["create", "read", "update", "delete"]
}
# Allow discovery of all policies
path "sys/policy/" {
capabilities = ["list"]
}
# Allow management of approle's with charm- prefix
path "auth/approle/role/charm-*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Allow discovery of approles
path "auth/approle/role" {
capabilities = ["read"]
}
path "auth/approle/role/" {
capabilities = ["list"]
}
# Allow charm- prefixes secrets backends to be mounted and managed
path "sys/mounts/charm-*" {
capabilities = ["create", "read", "update", "delete", "sudo"]
}
# Allow discovery of secrets backends
path "sys/mounts" {
capabilities = ["read"]
}
path "sys/mounts/" {
capabilities = ["list"]
}"""
VAULT_HEALTH_URL = '{vault_addr}/v1/sys/health'
VAULT_LOCALHOST_URL = "http://127.0.0.1:8220"
SECRET_BACKEND_HCL = """
path "{backend}/{hostname}/*" {{
capabilities = ["create", "read", "update", "delete", "list"]
}}
"""
SECRET_BACKEND_SHARED_HCL = """
path "{backend}/*" {{
capabilities = ["create", "read", "update", "delete", "list"]
}}
"""
def binding_address(binding):
try:
return hookenv.network_get_primary_address(binding)
except NotImplementedError:
return hookenv.unit_private_ip()
def get_vault_url(binding, port, address=None):
protocol = 'http'
ip = address or binding_address(binding)
if charms.reactive.is_state('vault.ssl.available'):
protocol = 'https'
return '{}://{}:{}'.format(protocol, ip, port)
get_api_url = functools.partial(get_vault_url,
binding='access', port=8200)
get_cluster_url = functools.partial(get_vault_url,
binding='cluster', port=8201)
def enable_approle_auth(client):
"""Enable the approle auth method within vault
:param client: Vault client
:type client: hvac.Client"""
if 'approle/' not in client.list_auth_backends():
client.enable_auth_backend('approle')
def create_local_charm_access_role(client, policies):
"""Create a role within vault associating the supplied policies
:param client: Vault client
:type client: hvac.Client
:param policies: List of policy names
:type policies: [str, str, ...]
:returns: Id of created role
:rtype: str"""
client.create_role(
CHARM_ACCESS_ROLE,
token_ttl='60s',
token_max_ttl='60s',
policies=policies,
bind_secret_id='false',
bound_cidr_list='127.0.0.1/32')
return client.get_role_id(CHARM_ACCESS_ROLE)
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')
client = hvac.Client(
url=VAULT_LOCALHOST_URL,
token=token)
enable_approle_auth(client)
policies = [CHARM_POLICY_NAME]
client.set_policy(CHARM_POLICY_NAME, CHARM_POLICY)
return create_local_charm_access_role(client, policies=policies)
def get_local_charm_access_role_id():
"""Retrieve the id of the role for local charm access
:returns: Id of local charm access role
:rtype: str
"""
return hookenv.leader_get(CHARM_ACCESS_ROLE_ID)
def get_client(url=None):
"""Provide a client for talking to the vault api
:returns: vault client
:rtype: hvac.Client
"""
return hvac.Client(url=url or 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=VAULT_LOCALHOST_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(url=VAULT_LOCALHOST_URL)
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(url=VAULT_LOCALHOST_URL)
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('totally-unsecure-auto-unlock'):
safe_restart = True
else:
client = get_client(url=VAULT_LOCALHOST_URL)
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
def configure_secret_backend(client, name):
"""Ensure a KV backend is enabled
:param client: Vault client
:ptype client: hvac.Client
:param name: Name of backend to enable
:ptype name: str"""
if '{}/'.format(name) not in client.list_secret_backends():
client.enable_secret_backend(backend_type='kv',
description='Charm created KV backend',
mount_point=name)
def configure_policy(client, name, hcl):
"""Create/update a role within vault associating the supplied policies
:param client: Vault client
:ptype client: hvac.Client
:param name: Name of policy to create
:ptype name: str
:param hcl: Vault policy HCL
:ptype hcl: str"""
client.set_policy(name, hcl)
def configure_approle(client, name, cidr, policies):
"""Create/update a role within vault associating the supplied policies
:param client: Vault client
:ptype client: hvac.Client
:param name: Name of role
:ptype name: str
:param cidr: Network address of remote unit
:ptype cidr: str
:param policies: List of policy names
:ptype policies: [str, str, ...]
:returns: Id of created role
:rtype: str"""
client.create_role(
name,
token_ttl='60s',
token_max_ttl='60s',
policies=policies,
bind_secret_id='false',
bound_cidr_list=cidr)
return client.get_role_id(name)