Количество микросервисов имеет тенденцию только увеличиваться, а ssl хочется для каждого сервиса, причём бесплатный. LetsEncrypt подходит не для всех случаев, ибо localnet, а easy-rsa надоел. 

Что такое Vault?

Vault - это инструмент для безопасного доступа к секретам. Секреты - это любые данные, к которым нужно жёстко контролировать доступ, например, API-ключи, пароли, сертификаты и многое другое. Vault предоставляет унифицированный RESTful API интерфейс доступа к секретам и позволяет вести подробный журнала аудита.

Vault генерирует и хранит секреты в бекендах (secret backends), коих имеется в достатке, но всё же маловато. Нас в данном случае интересует бекенд PKI - Public Key Infrastructure

Ограничения

Бекенды подключаются при помощи монтирования оных в точки монтирования, например, force.fm/pki/root, force.fm/secrets, и т.д. Для упрощения действий с сертификатами, каждой точке монтирования pki сопоставляется один ca certificate.

С версии 0.4 Vault бекенд PKI умеет генерировать самозаверенные корневые сертификаты, а также создавать CSR и подписи для промежуточных. Vault не поддерживает генерацию сертификатов с длиной ключа менее 2048 бит. Все приватные ключи могут быть получены только в момент их генерации. Для получения приватного ключа корневого сертификата, нужно его получать с типом exported, например, так:

# exported!
root_cert = client.write(
    join(R_CA_MOUNT, 'root/generate/exported'), common_name='Force FM Root CA',
    ttl='87600h', key_bits=KEY_BITS, exclude_cn_from_sans=True
)['data']

Если же private key не нужен для каких-то явно обозначенных целей, то лично я бы не стал его экспортировать, от греха по-дальше.

Начинаем

Подразумевается, что Vault проинициализирован и поднят, а также есть ключи распечатывания (unseal) оного, как и root token. Для примера будет использоваться домен trash.force.fm. Код написан с использованием hvac. Чтобы не рвать код на куски, он просто откомментирован. В целом процесс аналогичен easy-rsa.

Также Vault поддерживает CRL (документация, wiki).

Код

import os
import hvac
from os.path import join

# Здесь лежит наш скрипт.
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# Все ключи будем складывать в `keys`.
CERTS_DIR = join(BASE_DIR, 'keys')
# Для клиентских сертификатов, может пригодиться потом
CLIENT_CERTS_DIR = join(CERTS_DIR, 'clients')
os.makedirs(CLIENT_CERTS_DIR, exist_ok=True)

ROOT_TOKEN = 'azaza'
KEYS = [
  # Здесь unseal ключи, выданные Vault при vault init.    
  '123',
]

# Точка монтирования в Vault Root CA.
R_CA_MOUNT = 'force.fm/pki/root-ca'
# Точка монтирования в Vault Intermediate CA.
I_CA_MOUNT = 'force.fm/pki/intermediate-ca'
# установим интересующую нас длину ключей
KEY_BITS = 4096

# Пока подключаемся без TLS. Лучше, конечно, по локалнету.
VAULT_CONNECTION_OPTIONS = {
    'url': 'http://trash.force.fm:8200',
    'token': ROOT_TOKEN,
}
client = hvac.Client(**VAULT_CONNECTION_OPTIONS)

# С силой срываем пломбы.
client.is_sealed() and [client.unseal(k) for k in KEYS]

# Для удобства тестирования будем каждый раз убивать содержимое точек монтирования.
client.disable_secret_backend(R_CA_MOUNT)
client.disable_secret_backend(I_CA_MOUNT)

# Смонтируем PKI-бекенд для Root CA.
client.enable_secret_backend(
    'pki', 'Force FM Root CA', mount_point=R_CA_MOUNT, config={
        'max_lease_ttl': '87600h'
    }
)
client.write(
    join(R_CA_MOUNT, 'config/urls'),
    issuing_certificates=join(VAULT_CONNECTION_OPTIONS['url'], 'v1', R_CA_MOUNT, 'ca'),
    crl_distribution_points=join(VAULT_CONNECTION_OPTIONS['url'], 'v1', R_CA_MOUNT, 'crl')
)

# Смонтируем PKI-бекенд для промежуточного центра сертификации.
client.enable_secret_backend(
    'pki', 'Force FM Intermediate CA', mount_point=I_CA_MOUNT, config={
        'max_lease_ttl': '87600h',
    }
)
client.write(
    join(I_CA_MOUNT, 'config/urls'),
    issuing_certificates=join(VAULT_CONNECTION_OPTIONS['url'], 'v1', I_CA_MOUNT, 'ca'),
    crl_distribution_points=join(VAULT_CONNECTION_OPTIONS['url'], 'v1', I_CA_MOUNT, 'crl')
)

# Генерируем корневой CA cert.
root_cert = client.write(
    join(R_CA_MOUNT, 'root/generate/internal'), common_name='Force FM Root CA',
    ttl='87600h', key_bits=KEY_BITS, exclude_cn_from_sans=True
)['data']
assert root_cert['certificate'] == root_cert['issuing_ca']

# Сгенерируем Intermediate CSR от нашего Intermediate CA.
intermediate_csr = client.write(
    join(I_CA_MOUNT, 'intermediate/generate/internal'),
    common_name='Force FM Intermediate CA',
    ttl='87600h', key_bits=KEY_BITS, exclude_cn_from_sans=True
)['data']['csr']

# Получим у Root CA сертификат по выданному CSR.
# Чтобы не получить `cannot satisfy request, as TTL is beyond the expiration of the CA certificate`,
# сделаем ttl по-меньше, например, 8700h.
signed_cert = client.write(
    join(R_CA_MOUNT, 'root/sign-intermediate'), common_name='Force FM Intermediate CA',
    ttl='87000h', csr=intermediate_csr
)['data']
assert signed_cert['issuing_ca'] == root_cert['certificate'] == root_cert['issuing_ca']

# Если забыть записать подписанный Root CA сертификат в список подписанных в Intermediate CA,
# то вызывая issue/web-server можно получить очень содержательную ошибку вида:
# `Error fetching CA certificate: stored CA information not able to be parsed`.
client.write(join(I_CA_MOUNT, 'intermediate/set-signed'), certificate=signed_cert['certificate'])

# Создадим роль Vault `web-server`, которая будет выдавать нам сертификаты от имени
# нашего Intermediate CA. После записи в issue/web-server мы будем получать по одному сертификату.
client.write(
    join(I_CA_MOUNT, 'roles/web-server'),
    key_bits=KEY_BITS, max_ttl='86000h', allow_any_name=True
)

# Выпустим сертификат от Intermediate CA.
cert = client.write(
    join(I_CA_MOUNT, 'issue/web-server'),
    common_name='trash.force.fm', ttl='86000h'
)['data']

# Сохраним сертификат Root CA.
with open(join(CERTS_DIR, 'force.fm__root_ca.crt'), 'w') as f:
    f.write(root_cert['certificate'])
    
with open(join(CERTS_DIR, 'force.fm__intermediate.csr'), 'w') as f:
    f.write(intermediate_csr)

with open(join(CERTS_DIR, 'force.fm__signed_cert.csr'), 'w') as f:
    f.write(signed_cert['certificate'])

with open(join(CERTS_DIR, 'trash.force.fm__ca_chain.crt'), 'w') as f:
    f.write(''.join(cert['ca_chain']))

with open(join(CERTS_DIR, 'trash.force.fm.crt'), 'w') as f:
    f.write(cert['certificate'])

with open(join(CERTS_DIR, 'trash.force.fm__issuing_ca.crt'), 'w') as f:
    f.write(cert['issuing_ca'])

with open(join(CERTS_DIR, 'trash.force.fm.key'), 'w') as f:
    f.write(cert['private_key'])

with open(join(CERTS_DIR, 'trash.force.fm__bundle.crt'), 'w') as f:
    f.write(cert['certificate'] + '\n' + cert['issuing_ca'])

Заставляем Vault использовать его же сертификаты

Полученные сертификаты можно использовать для самого Vault. Для этого сделаем mkdir /etc/vault/certs/ и скопируем туда 2 файла:

  • trash.force.fm__bundle.crt
  • trash.force.fm.key

В bundle записаны 2 сертификата, как того требует мануал Vault: основной сертификат и issuing ca сертификат. В config.hcl допишем что-то типа:

listener "tcp" {
    address = "0.0.0.0:18400"
    #tls_disable = 1
    tls_cert_file = "/etc/vault/certs/trash.force.fm__bundle.crt"
    tls_key_file = "/etc/vault/certs/trash.force.fm.key"
}

Теперь скопируем на локальную машину корневой сертификат force.fm__root_ca.crt и укажем его в verify, ибо он у нас самозаверенный, а то нас накажут при помощи requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:720).

VAULT_CONNECTION_OPTIONS = {
    'url': 'https://trash.force.fm:18400',
    'token': ROOT_TOKEN,    
    'verify': join(CERTS_DIR, 'force.fm__root_ca.crt'),
}
client = hvac.Client(**VAULT_CONNECTION_OPTIONS)

Можно сгенерировать и клиентский сертификат на месте:

cert = client.write(
    join(I_CA_MOUNT, 'issue/web-server'),
    common_name='client1', ttl='86000h'
)['data']
with open(join(CLIENT_CERTS_DIR, 'client1.key'), 'w') as f:
    f.write(cert['private_key'])
with open(join(CLIENT_CERTS_DIR, 'client1__bundle.crt'), 'w') as f:
    f.write(cert['certificate'] + '\n' + cert['issuing_ca'])

Тогда подключение будет выглядеть примерно так:

VAULT_CONNECTION_OPTIONS = {
    'url': 'https://trash.force.fm:18400',
    'token': ROOT_TOKEN,
    'cert': (
        join(CLIENT_CERTS_DIR, 'client1__bundle.crt'),
        join(CLIENT_CERTS_DIR, 'client1.key')
    ),
    'verify': join(CERTS_DIR, 'force.fm__root_ca.crt'),
}
client = hvac.Client(**VAULT_CONNECTION_OPTIONS)
night-crawler
Просмотров: 221
blog comments powered by Disqus