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

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
from CommandRunner import CommandRunner
from KubernetesSecretsManager import KubernetesSecretsManager
#def run_command(command):
# try:
@ -303,19 +304,31 @@ def main(token: str):
# Create a role
role_id, secret_id = create_app_role(role_name, policy_name)
# Save the role_id and secret_id to a backup file
save_role_vars_to_backup_file(role_name, role_id, secret_id)
if os.environ.get('MODE') == 'file':
# 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_role_vars_to_file(role_id, secret_id)
# Save the role_id and secret_id to a file
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:
# Get the existing role_id and secret_id
role_id = get_role_id(role_name)
secret_id = get_secret_id(role_name)
# Save the role_id and secret_id to a file
# QUESTION: Should this be conditional on if the file already exists or not?
save_role_vars_to_file(role_id, secret_id)
if os.environ.get('MODE') == 'file':
# Save the role_id and secret_id to a file
# 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')

View file

@ -1,5 +1,6 @@
import os, json
from CommandRunner import CommandRunner
from KubernetesSecretsManager import KubernetesSecretsManager
class Initializer:
def check_if_initialized(self) -> bool:
@ -83,8 +84,13 @@ class Initializer:
# UPDATE: Is mounted as a volume instead
#CommandRunner.run_command('mkdir /vault/creds')
self.create_unseal_keys_file()
self.create_root_token_file()
if os.environ.get('MODE') == '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:
"""Check if the vault is sealed or not
@ -164,7 +170,7 @@ class Initializer:
print(f'Policy Capabilities: {os.getenv("POLICY_CAPABILITIES")}')
# 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():
initializer = Initializer()
@ -174,11 +180,15 @@ def main():
if not initializer.check_if_initialized():
initializer.init_vault()
# This is just a safety check/measure to ensure the script can continue
# The only time this would likely be triggered is if there was some kind of PV desyncronization but it shouldn't really happen.
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.')
if os.environ.get('MODE') == 'file':
# This is just a safety check / measure to ensure the script can continue
# The only time this would likely be triggered is if there was some kind of PV desyncronization but it shouldn't really happen.
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)
if initializer.is_vault_sealed():
initializer.unseal_vault()

View file

@ -0,0 +1 @@
kubernetes