Attempting to add support to use Kubernetes secrets for vault root token, unseal keys and app role data
All checks were successful
Build and deploy Bridgeman Accessible Hashicorp Vault Implementation / deploy (push) Successful in 2m48s
All checks were successful
Build and deploy Bridgeman Accessible Hashicorp Vault Implementation / deploy (push) Successful in 2m48s
This commit is contained in:
parent
f8cb28246f
commit
dd5a8abd55
7 changed files with 171 additions and 21 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -2,4 +2,7 @@
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Ignore VSCode settings (that don't need to be shared)
|
# Ignore VSCode settings (that don't need to be shared)
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Ignore Python virtual environment
|
||||||
|
.venv
|
||||||
14
Dockerfile
14
Dockerfile
|
|
@ -1,5 +1,7 @@
|
||||||
FROM hashicorp/vault:latest
|
FROM hashicorp/vault:latest
|
||||||
|
|
||||||
|
WORKDIR /vault/setup
|
||||||
|
|
||||||
# Install Bash
|
# Install Bash
|
||||||
RUN apk add --no-cache --upgrade bash
|
RUN apk add --no-cache --upgrade bash
|
||||||
|
|
||||||
|
|
@ -11,6 +13,10 @@ RUN python3 -m venv .venv \
|
||||||
&& python -m ensurepip \
|
&& python -m ensurepip \
|
||||||
&& pip install --no-cache --upgrade pip setuptools
|
&& pip install --no-cache --upgrade pip setuptools
|
||||||
|
|
||||||
|
# Install any needed dependencies
|
||||||
|
COPY ./setup-scripts/requirements.txt ./
|
||||||
|
RUN source .venv/bin/activate && pip install --no-cache -r requirements.txt
|
||||||
|
|
||||||
# Needed for parsing JSON in Bash (which is needed to parse the unseal keys and root token)
|
# Needed for parsing JSON in Bash (which is needed to parse the unseal keys and root token)
|
||||||
RUN apk add --no-cache jq
|
RUN apk add --no-cache jq
|
||||||
|
|
||||||
|
|
@ -18,11 +24,11 @@ RUN apk add --no-cache jq
|
||||||
COPY vault-config.hcl /vault/config/vault-config.hcl
|
COPY vault-config.hcl /vault/config/vault-config.hcl
|
||||||
|
|
||||||
# Copy the startup script into the container (also verifying it's encoded properly)
|
# Copy the startup script into the container (also verifying it's encoded properly)
|
||||||
COPY ./entrypoint.sh /entrypoint.sh
|
COPY ./entrypoint.sh ./
|
||||||
RUN dos2unix /entrypoint.sh
|
RUN dos2unix ./entrypoint.sh
|
||||||
|
|
||||||
# Copy the Python startup stuff into the container
|
# Copy the Python startup stuff into the container
|
||||||
COPY ./setup-scripts /setup-scripts
|
COPY ./setup-scripts ./setup-scripts
|
||||||
|
|
||||||
# Copy the snapshot server Python code into the container
|
# Copy the snapshot server Python code into the container
|
||||||
COPY ./snapshot-server /snapshot-server
|
COPY ./snapshot-server /snapshot-server
|
||||||
|
|
@ -33,4 +39,4 @@ COPY ./snapshot-server /snapshot-server
|
||||||
# | 8300 | Custom snapshot server (for creating and serving backups over HTTP) |
|
# | 8300 | Custom snapshot server (for creating and serving backups over HTTP) |
|
||||||
EXPOSE 8200 8300
|
EXPOSE 8200 8300
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
|
ENTRYPOINT ["/bin/bash", "/vault/setup/entrypoint.sh"]
|
||||||
|
|
@ -123,7 +123,7 @@ start_and_wait_for_vault() {
|
||||||
|
|
||||||
start_and_wait_for_vault
|
start_and_wait_for_vault
|
||||||
|
|
||||||
python3 /setup-scripts/prod-setup.py
|
source .venv/bin/activate && python3 /setup-scripts/prod-setup.py
|
||||||
|
|
||||||
#init_vault
|
#init_vault
|
||||||
#unseal_vault
|
#unseal_vault
|
||||||
|
|
|
||||||
117
setup-scripts/KubernetesSecretsManager.py
Normal file
117
setup-scripts/KubernetesSecretsManager.py
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
from kubernetes import client, config
|
||||||
|
from kubernetes.client.rest import ApiException
|
||||||
|
import os
|
||||||
|
|
||||||
|
class KubernetesSecretManager:
|
||||||
|
def __init__(self):
|
||||||
|
# This magic line automatically reads the KSA token mounted at
|
||||||
|
# /var/run/secrets/kubernetes.io/serviceaccount/token inside the pod!
|
||||||
|
config.load_incluster_config()
|
||||||
|
self.api_instance = client.CoreV1Api()
|
||||||
|
|
||||||
|
# Read namespace from standard K8s environment variable, default to 'default'
|
||||||
|
self.namespace = os.environ.get('POD_NAMESPACE', 'default')
|
||||||
|
|
||||||
|
def publish_unseal_keys_credentials(self, secret_name: str, unseal_keys: list):
|
||||||
|
"""Publish the unseal keys as a Kubernetes Secret
|
||||||
|
|
||||||
|
Args:
|
||||||
|
secret_name (str): The name of the Kubernetes Secret to create
|
||||||
|
unseal_keys (list): The vault's unseal keys
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the secret payload using string_data (K8s auto-base64 encodes it for us)
|
||||||
|
secret_body = client.V1Secret(
|
||||||
|
metadata=client.V1ObjectMeta(name=secret_name),
|
||||||
|
type="Opaque",
|
||||||
|
string_data={
|
||||||
|
f'unseal_key_{i}': key for i, key in enumerate(unseal_keys)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to create the secret
|
||||||
|
self.api_instance.create_namespaced_secret(
|
||||||
|
namespace=self.namespace,
|
||||||
|
body=secret_body
|
||||||
|
)
|
||||||
|
print("[SUCCESS] Kubernetes Secret created.")
|
||||||
|
|
||||||
|
except ApiException as e:
|
||||||
|
# Status 409 means the Secret already exists
|
||||||
|
if e.status == 409:
|
||||||
|
print("[ERROR] Secret already exists.")
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Failed to publish K8s Secret: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def publish_root_token_credentials(self, secret_name: str, root_token: str):
|
||||||
|
"""Publish the root token as a Kubernetes Secret
|
||||||
|
|
||||||
|
Args:
|
||||||
|
secret_name (str): The name of the Kubernetes Secret to create
|
||||||
|
root_token (str): The vault's root token
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the secret payload using string_data (K8s auto-base64 encodes it for us)
|
||||||
|
secret_body = client.V1Secret(
|
||||||
|
metadata=client.V1ObjectMeta(name=secret_name),
|
||||||
|
type="Opaque",
|
||||||
|
string_data={
|
||||||
|
'root_token': root_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to create the secret
|
||||||
|
self.api_instance.create_namespaced_secret(
|
||||||
|
namespace=self.namespace,
|
||||||
|
body=secret_body
|
||||||
|
)
|
||||||
|
print("[SUCCESS] Kubernetes Secret created.")
|
||||||
|
|
||||||
|
except ApiException as e:
|
||||||
|
# Status 409 means the Secret already exists
|
||||||
|
if e.status == 409:
|
||||||
|
print("[ERROR] Secret already exists.")
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Failed to publish K8s Secret: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def publish_approle_credentials(self, secret_name: str, role_id: str, secret_id: str):
|
||||||
|
"""
|
||||||
|
Creates or updates a Kubernetes Secret with the Vault AppRole credentials.
|
||||||
|
"""
|
||||||
|
print(f"Publishing AppRole credentials to Kubernetes Secret: {secret_name}")
|
||||||
|
|
||||||
|
# Define the secret payload using string_data (K8s auto-base64 encodes it for us)
|
||||||
|
secret_body = client.V1Secret(
|
||||||
|
metadata=client.V1ObjectMeta(name=secret_name),
|
||||||
|
type="Opaque",
|
||||||
|
string_data={
|
||||||
|
"role_id": role_id,
|
||||||
|
"secret_id": secret_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to create the secret
|
||||||
|
self.api_instance.create_namespaced_secret(
|
||||||
|
namespace=self.namespace,
|
||||||
|
body=secret_body
|
||||||
|
)
|
||||||
|
print("[SUCCESS] Kubernetes Secret created.")
|
||||||
|
|
||||||
|
except ApiException as e:
|
||||||
|
# Status 409 means the Secret already exists
|
||||||
|
if e.status == 409:
|
||||||
|
print("[INFO] Secret already exists. Patching with new credentials...")
|
||||||
|
self.api_instance.patch_namespaced_secret(
|
||||||
|
name=secret_name,
|
||||||
|
namespace=self.namespace,
|
||||||
|
body=secret_body
|
||||||
|
)
|
||||||
|
print("[SUCCESS] Kubernetes Secret updated.")
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Failed to publish K8s Secret: {e}")
|
||||||
|
raise e
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
import os, sys, subprocess, json, logging
|
import os, sys, subprocess, json, logging
|
||||||
|
|
||||||
from CommandRunner import CommandRunner
|
from CommandRunner import CommandRunner
|
||||||
|
from KubernetesSecretsManager import KubernetesSecretsManager
|
||||||
|
|
||||||
#def run_command(command):
|
#def run_command(command):
|
||||||
# try:
|
# try:
|
||||||
|
|
@ -303,19 +304,31 @@ def main(token: str):
|
||||||
# Create a role
|
# Create a role
|
||||||
role_id, secret_id = create_app_role(role_name, policy_name)
|
role_id, secret_id = create_app_role(role_name, policy_name)
|
||||||
|
|
||||||
# Save the role_id and secret_id to a backup file
|
if os.environ.get('MODE') == 'file':
|
||||||
save_role_vars_to_backup_file(role_name, role_id, secret_id)
|
# Save the role_id and secret_id to a backup file
|
||||||
|
save_role_vars_to_backup_file(role_name, role_id, secret_id)
|
||||||
|
|
||||||
# Save the role_id and secret_id to a file
|
# Save the role_id and secret_id to a file
|
||||||
save_role_vars_to_file(role_id, secret_id)
|
save_role_vars_to_file(role_id, secret_id)
|
||||||
|
else:
|
||||||
|
# In Kubernetes mode we will create Kubernetes secrets with the role_id and secret_id instead of writing to files
|
||||||
|
k8s_manager = KubernetesSecretsManager()
|
||||||
|
if os.environ.get('ROLE_ID_SECRET_NAME') and os.environ.get('SECRET_ID_SECRET_NAME') and os.environ.get('ROLE_ID_SECRET_NAME') == os.environ.get('SECRET_ID_SECRET_NAME'):
|
||||||
|
k8s_manager.publish_approle_credentials(os.environ.get('ROLE_ID_SECRET_NAME'), role_id, secret_id)
|
||||||
else:
|
else:
|
||||||
# Get the existing role_id and secret_id
|
# Get the existing role_id and secret_id
|
||||||
role_id = get_role_id(role_name)
|
role_id = get_role_id(role_name)
|
||||||
secret_id = get_secret_id(role_name)
|
secret_id = get_secret_id(role_name)
|
||||||
|
|
||||||
# Save the role_id and secret_id to a file
|
if os.environ.get('MODE') == 'file':
|
||||||
# QUESTION: Should this be conditional on if the file already exists or not?
|
# Save the role_id and secret_id to a file
|
||||||
save_role_vars_to_file(role_id, secret_id)
|
# QUESTION: Should this be conditional on if the file already exists or not?
|
||||||
|
save_role_vars_to_file(role_id, secret_id)
|
||||||
|
else:
|
||||||
|
# In Kubernetes mode we will create Kubernetes secrets with the role_id and secret_id instead of writing to files
|
||||||
|
k8s_manager = KubernetesSecretsManager()
|
||||||
|
if os.environ.get('ROLE_ID_SECRET_NAME') and os.environ.get('SECRET_ID_SECRET_NAME') and os.environ.get('ROLE_ID_SECRET_NAME') == os.environ.get('SECRET_ID_SECRET_NAME'):
|
||||||
|
k8s_manager.publish_approle_credentials(os.environ.get('ROLE_ID_SECRET_NAME'), role_id, secret_id)
|
||||||
|
|
||||||
logging.info('AppRole setup complete')
|
logging.info('AppRole setup complete')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import os, json
|
import os, json
|
||||||
from CommandRunner import CommandRunner
|
from CommandRunner import CommandRunner
|
||||||
|
from KubernetesSecretsManager import KubernetesSecretsManager
|
||||||
|
|
||||||
class Initializer:
|
class Initializer:
|
||||||
def check_if_initialized(self) -> bool:
|
def check_if_initialized(self) -> bool:
|
||||||
|
|
@ -83,8 +84,13 @@ class Initializer:
|
||||||
# UPDATE: Is mounted as a volume instead
|
# UPDATE: Is mounted as a volume instead
|
||||||
#CommandRunner.run_command('mkdir /vault/creds')
|
#CommandRunner.run_command('mkdir /vault/creds')
|
||||||
|
|
||||||
self.create_unseal_keys_file()
|
if os.environ.get('MODE') == 'file':
|
||||||
self.create_root_token_file()
|
self.create_unseal_keys_file()
|
||||||
|
self.create_root_token_file()
|
||||||
|
else:
|
||||||
|
k8s_manager = KubernetesSecretsManager()
|
||||||
|
k8s_manager.publish_unseal_keys_credentials('vault-unseal-keys', self.unseal_keys)
|
||||||
|
k8s_manager.publish_root_token_credentials('vault-root-token', self.root_token)
|
||||||
|
|
||||||
def is_vault_sealed(self) -> bool:
|
def is_vault_sealed(self) -> bool:
|
||||||
"""Check if the vault is sealed or not
|
"""Check if the vault is sealed or not
|
||||||
|
|
@ -164,7 +170,7 @@ class Initializer:
|
||||||
print(f'Policy Capabilities: {os.getenv("POLICY_CAPABILITIES")}')
|
print(f'Policy Capabilities: {os.getenv("POLICY_CAPABILITIES")}')
|
||||||
|
|
||||||
# Run the custom entrypoint Python script
|
# Run the custom entrypoint Python script
|
||||||
CommandRunner.run_command_in_real_time(f'python3 /setup-scripts/app-role-access.py {self.root_token}')
|
CommandRunner.run_command_in_real_time(f'source .venv/bin/activate && python3 /vault/setup/setup-scripts/app-role-access.py {self.root_token}')
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
initializer = Initializer()
|
initializer = Initializer()
|
||||||
|
|
@ -174,11 +180,15 @@ def main():
|
||||||
if not initializer.check_if_initialized():
|
if not initializer.check_if_initialized():
|
||||||
initializer.init_vault()
|
initializer.init_vault()
|
||||||
|
|
||||||
# This is just a safety check/measure to ensure the script can continue
|
if os.environ.get('MODE') == 'file':
|
||||||
# The only time this would likely be triggered is if there was some kind of PV desyncronization but it shouldn't really happen.
|
# This is just a safety check / measure to ensure the script can continue
|
||||||
if not initializer.check_root_token_and_unseal_keys_files_exist():
|
# The only time this would likely be triggered is if there was some kind of PV desyncronization but it shouldn't really happen.
|
||||||
raise RuntimeError('Vault is in an inconsistent state for this script to continue. Please ensure the vault can be initialized OR both the root token and unseal keys files exist alongside and initialized vault.')
|
if not initializer.check_root_token_and_unseal_keys_files_exist():
|
||||||
|
raise RuntimeError('Vault is in an inconsistent state for this script to continue. Please ensure the vault can be initialized OR both the root token and unseal keys files exist alongside and initialized vault.')
|
||||||
|
else:
|
||||||
|
# In Kubernetes mode we expect the secrets to be created so we can pull them back down if needed, but we don't necessarily need to check for them here since we aren't relying on files for the credentials in this mode.
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if the vault is sealed (as we need to unseal it to set it up)
|
# Check if the vault is sealed (as we need to unseal it to set it up)
|
||||||
if initializer.is_vault_sealed():
|
if initializer.is_vault_sealed():
|
||||||
initializer.unseal_vault()
|
initializer.unseal_vault()
|
||||||
|
|
|
||||||
1
setup-scripts/requirements.txt
Normal file
1
setup-scripts/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
kubernetes
|
||||||
Loading…
Add table
Add a link
Reference in a new issue