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

This commit is contained in:
Alan Bridgeman 2026-04-05 15:38:21 -05:00
parent f8cb28246f
commit dd5a8abd55
7 changed files with 171 additions and 21 deletions

5
.gitignore vendored
View file

@ -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

View file

@ -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"]

View file

@ -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

View 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

View file

@ -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')

View file

@ -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()

View file

@ -0,0 +1 @@
kubernetes