Skip to content
Snippets Groups Projects
anvil.py 8.53 KiB
#!/usr/bin/env python3
"""Anvil,
   wile_coyote test tool, erases, uploads and checks certificates
   validity onto test instances of Vault, Redis and Consul.
   In order to use Anvil you need to define a list of certificates
   that you always expect to find in Vault, Redis and Consul.

Usage:
  anvil.py [--prune]
  anvil.py (-h | --help)

Options:
  -h --help    Show this screen.
  -p --prune   Delete local certificates and fetch them again
"""
import os
import re
from glob import glob
import subprocess as sp
from docopt import docopt
import redis
from consul import Consul
import hvac
import wile_coyote.tools
import wile_coyote.common_kit
from wile_coyote.common_kit import log


def certificates_delete(provider: str, certificates: list):
    """ delete list of certificates from a given provider """
    for cert in certificates:
        cbot_cmd = '/usr/local/bin/certbot delete -non-interactive' \
            + f' -c /etc/${provider}/cli.ini --cert-name ${cert}'
        cbot_child = sp.Popen(cbot_cmd.split(), stdout=sp.PIPE, stderr=sp.PIPE)
        cbot_out, cbot_err = cbot_child.communicate()
        if cbot_child.returncode != 0:
            err = cbot_err.decode("utf-8")
            log.handler(f"error executing certbot: {err}", LOGFILE, True)
            os.sys.exit(1)
        else:
            decoded_msg = cbot_out.decode("utf-8")
            msg = decoded_msg[:decoded_msg.rfind('\n')]
            log.handler(msg, LOGFILE)


def redis_keys(server, token):
    """ download keys from Redis """
    log.handler('fetching keys from Redis...', LOGFILE)
    r_client = redis.StrictRedis(
        host=server, password=token, port=6379, db=0)

    try:
        r_keys = [n.decode("utf-8") for n in r_client.keys('*')]
    except Exception as err:  # pylint: disable=w0703
        r_keys = err

    return sorted(r_keys)


def redis_prune(server, token):
    """ prune keys from Redis """
    log.handler('purging keys from Redis...', LOGFILE)
    r_client = redis.StrictRedis(host=server, password=token, port=6379, db=0)
    r_keys = [n.decode("utf-8") for n in r_client.keys('*')]
    for key in r_keys:
        r_client.delete(key)


def vault_keys(server, token, mount_points):
    """ download key from vault """
    log.handler('fetching keys from Vault...', LOGFILE)
    all_vault_keys = []
    for mount in mount_points:
        vault_cmd = f'/usr/local/bin/rvault --token {token}' \
            + f' --insecure --address https://{server} list {mount}/'
        vault_child = sp.Popen(
            vault_cmd.split(), stdout=sp.PIPE, stderr=sp.PIPE)
        vault_out, vault_err = vault_child.communicate()
        if vault_child.returncode != 0:
            err = vault_err.decode("utf-8")
            log.handler(f'error listing keys for the mount point {mount}: {err}', LOGFILE)
            os.sys.exit(1)
        else:
            decoded_msg = vault_out.decode("utf-8")
            _msg = [x for x in decoded_msg.split('\n') if x != '']
            msg = [f'/{mount}{x}' for x in _msg]
        all_vault_keys.extend(msg)

    return sorted(all_vault_keys)


def vault_prune(server, token, mount_points, ver):
    """ prune key to vault """
    log.handler('purging keys from Vault...')
    v_client = hvac.Client(url=f'https://{server}', token=token)
    for mount in mount_points:
        v_client.sys.disable_secrets_engine(mount)
        v_client.sys.enable_secrets_engine(
            'kv', path=mount, options={'version': ver})


def consul_keys(server, token):
    """ download keys from consul """
    log.handler('fetching keys from Consul...', LOGFILE)
    c_client = Consul(host=server, port='443', token=token, scheme='https')
    try:
        c_keys = sorted(c_client.kv.get('nomad', recurse=True, keys=True)[1])
    except Exception as err:  # pylint: disable=W0703
        return err

    return sorted(c_keys)


def consul_prune(server, token):
    """ prune keys from consul """
    log.handler('purging keys from Consul...', LOGFILE)
    c_client = Consul(host=server, port='443', token=token, scheme='https')
    for nomad_env in ['test', 'uat', 'prod']:
        c_client.kv.delete(f'nomad/{nomad_env}', recurse=True)


# Here we Go.
if __name__ == "__main__":

    ARGS = docopt(__doc__)
    PRUNE = ARGS['--prune']
    LOGFILE = '/dev/stdout'

    # Anvil should not be used in production
    VAULT_ROOT_TOKEN = wile_coyote.tools.VAULT_ROOT_TOKEN
    if not VAULT_ROOT_TOKEN:
        log.handler('you can use this tool ONLY in test', LOGFILE, True)
        log.handler('exiting ...', LOGFILE, True)
        os.sys.exit()

    REDIS_HOST = wile_coyote.tools.REDIS_HOST
    REDIS_TOKEN = wile_coyote.tools.REDIS_TOKEN
    VAULT_HOST = wile_coyote.tools.VAULT_HOST
    CONSUL_SERVERS = wile_coyote.tools.CONSUL_SERVERS
    CONSUL_TOKEN = wile_coyote.tools.CONSUL_TOKEN
    CONSUL_LEADER, _, __ = wile_coyote.tools.consul_leader.get(LOGFILE)
    MOUNT_POINTS_V1 = wile_coyote.tools.MOUNT_POINTS_V1
    MOUNT_POINTS_V2 = wile_coyote.tools.MOUNT_POINTS_V2

    # keys define in .acme.ini
    REDIS_KEYS = wile_coyote.tools.REDIS_KEYS
    VAULT_KEYS = wile_coyote.tools.VAULT_KEYS
    CONSUL_KEYS = wile_coyote.tools.CONSUL_KEYS

    # delete and upload keys again
    consul_prune(CONSUL_LEADER, CONSUL_TOKEN)
    redis_prune(REDIS_HOST, REDIS_TOKEN)
    vault_prune(VAULT_HOST, VAULT_ROOT_TOKEN, MOUNT_POINTS_V1, 1)
    vault_prune(VAULT_HOST, VAULT_ROOT_TOKEN, MOUNT_POINTS_V2, 2)

    # prune certificates locally
    ACME_PROVIDERS = ['sectigo_ev', 'sectigo_ev', 'letsencrypt']
    for acme_provider in ACME_PROVIDERS:
        acme_certificates = glob(f'/etc/{acme_provider}/live/*')
        try:
            acme_certificates.remove(f'/etc/{acme_provider}/live/README')
        except ValueError:
            pass
        certificates_delete(acme_provider, acme_certificates)

    # run all scripts under /opt/acme/bin
    for script in glob('/opt/acme/bin/*'):
        log.handler(f'running script {script}', LOGFILE)
        os.system(script)

    # upstream keys
    C_KEYS = consul_keys(CONSUL_LEADER, CONSUL_TOKEN)
    R_KEYS = redis_keys(REDIS_HOST, REDIS_TOKEN)
    V_KEYS_V1 = vault_keys(VAULT_HOST, VAULT_ROOT_TOKEN, MOUNT_POINTS_V1)
    V_KEYS_V2 = vault_keys(VAULT_HOST, VAULT_ROOT_TOKEN, MOUNT_POINTS_V2)
    V_KEYS_ALL = sorted(V_KEYS_V1 + V_KEYS_V2)

    # differences
    C_DIFFS = [item for item in CONSUL_KEYS if item not in C_KEYS]
    R_DIFFS = [item for item in REDIS_KEYS if item not in R_KEYS]
    V_DIFFS = [item for item in VAULT_KEYS if item not in V_KEYS_ALL]
    for key_store in ['Consul', 'Redis', 'Vault']:
        if key_store == 'Consul':
            key_list = C_DIFFS
        elif key_store == 'Redis':
            key_list = R_DIFFS
        elif key_store == 'Vault':
            key_list = V_DIFFS
        if key_list:
            log.handler(f'the following keys are not available on {key_store}:', LOGFILE, True)
            log.handler(' '.join(key_list), LOGFILE, True)
            os.sys.exit(1)
        else:
            log.handler(f'{key_store} keys names have been successfully checked')

    if REDIS_KEYS != R_KEYS:
        log.handler('downstream and upstream Redis keys are different', LOGFILE, True)
        os.sys.exit(1)
    elif CONSUL_KEYS != C_KEYS:
        log.handler('downstream and upstream Consul keys are different', LOGFILE, True)
        os.sys.exit(1)
    elif VAULT_KEYS != V_KEYS_ALL:
        log.handler('downstream and upstream Vault keys are different', LOGFILE, True)
        os.sys.exit(1)

    # validate public keys
    for r_key in [item for item in REDIS_KEYS if '_chain.pem' not in R_KEYS]:
        log.handler(f'checking certificate {r_key} on Redis', LOGFILE)
        pubkey = wile_coyote.tools.redis_get.get(r_key)
        wile_coyote.common_kit.public_key.check(pubkey, LOGFILE, r_key, False)

    for c_key in [item for item in CONSUL_KEYS if '_chain.pem' not in C_KEYS]:
        log.handler(f'checking certificate {c_key} on Consul', LOGFILE)
        pubkey = wile_coyote.tools.consul_get.get(c_key)
        wile_coyote.common_kit.public_key.check(pubkey, LOGFILE, c_key, False)

    # validate private keys: we check only if it's not malformed
    for v_key in V_KEYS_V1:
        log.handler(f'checking private key {v_key} on Vault', LOGFILE)
        privkey = wile_coyote.tools.vault_get.get(v_key, 'root')
        wile_coyote.common_kit.public_key.check(privkey, LOGFILE, v_key, False)

    for v_key in V_KEYS_V2:
        log.handler(f'checking private key {v_key} on Vault v2', LOGFILE)
        fixed_v_key = re.sub(r'^/nomad/', '/', v_key)
        privkey = wile_coyote.tools.vault_get_v2.get(fixed_v_key, 'root')
        wile_coyote.common_kit.public_key.check(privkey, LOGFILE, v_key, False)