mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-05-18 10:40:33 +02:00
Updated Vault Creation script
This commit is contained in:
parent
e8305fa598
commit
9eca1958ec
@ -1,4 +1,4 @@
|
|||||||
import yaml
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import secrets
|
import secrets
|
||||||
import hashlib
|
import hashlib
|
||||||
@ -6,148 +6,194 @@ import bcrypt
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def prompt(text, default=None):
|
import yaml
|
||||||
"""Prompt the user for input, with optional default value."""
|
from yaml.loader import SafeLoader
|
||||||
prompt_text = f"[?] {text}" + (f" [{default}]" if default else "") + ": "
|
|
||||||
response = input(prompt_text)
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
return response.strip() or default
|
# Let PyYAML treat !vault blocks as ordinary multiline strings
|
||||||
|
def _vault_constructor(loader, node):
|
||||||
|
return node.value
|
||||||
|
SafeLoader.add_constructor('!vault', _vault_constructor)
|
||||||
|
|
||||||
|
# VaultScalar so PyYAML emits a !vault literal block on output
|
||||||
|
class VaultScalar(str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _vault_representer(dumper, data):
|
||||||
|
return dumper.represent_scalar('!vault', data, style='|')
|
||||||
|
|
||||||
|
yaml.SafeDumper.add_representer(VaultScalar, _vault_representer)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def generate_value(algorithm):
|
def generate_value(algorithm):
|
||||||
"""Generate a value based on the provided algorithm."""
|
|
||||||
if algorithm == "random_hex":
|
if algorithm == "random_hex":
|
||||||
return secrets.token_hex(64)
|
return secrets.token_hex(64)
|
||||||
elif algorithm == "sha256":
|
if algorithm == "sha256":
|
||||||
return hashlib.sha256(secrets.token_bytes(32)).hexdigest()
|
return hashlib.sha256(secrets.token_bytes(32)).hexdigest()
|
||||||
elif algorithm == "sha1":
|
if algorithm == "sha1":
|
||||||
return hashlib.sha1(secrets.token_bytes(20)).hexdigest()
|
return hashlib.sha1(secrets.token_bytes(20)).hexdigest()
|
||||||
elif algorithm == "bcrypt":
|
if algorithm == "bcrypt":
|
||||||
password = secrets.token_urlsafe(16).encode()
|
pw = secrets.token_urlsafe(16).encode()
|
||||||
return bcrypt.hashpw(password, bcrypt.gensalt()).decode()
|
return bcrypt.hashpw(pw, bcrypt.gensalt()).decode()
|
||||||
elif algorithm == "plain":
|
if algorithm == "plain":
|
||||||
return secrets.token_urlsafe(32)
|
return secrets.token_urlsafe(32)
|
||||||
else:
|
return "undefined"
|
||||||
return "undefined"
|
|
||||||
|
|
||||||
def encrypt_with_vault(value, name, vault_password_file=None, ask_vault_pass=False):
|
def decrypt_inventory(path: Path, vault_password_file: str):
|
||||||
"""Encrypt the given string using Ansible Vault."""
|
"""
|
||||||
cmd = ["ansible-vault", "encrypt_string", value, f"--name={name}"]
|
Try `ansible-vault view`; on "not vaulted" fallback to yaml.safe_load
|
||||||
if vault_password_file:
|
(with !vault constructor).
|
||||||
cmd += ["--vault-password-file", vault_password_file]
|
"""
|
||||||
elif ask_vault_pass:
|
proc = subprocess.run(
|
||||||
cmd += ["--ask-vault-pass"]
|
["ansible-vault", "view", str(path), "--vault-password-file", vault_password_file],
|
||||||
else:
|
capture_output=True, text=True
|
||||||
raise RuntimeError("You must provide --vault-password-file or use --ask-vault-pass.")
|
)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return yaml.safe_load(proc.stdout) or {}
|
||||||
|
# fallback if not vaulted
|
||||||
|
if "not a vault encrypted file" in proc.stderr.lower():
|
||||||
|
return yaml.safe_load(path.read_text()) or {}
|
||||||
|
raise RuntimeError(f"ansible-vault view failed:\n{proc.stderr}")
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
def encrypt_with_vault(value: str, name: str, vault_password_file: str):
|
||||||
if result.returncode != 0:
|
cmd = [
|
||||||
raise RuntimeError(f"Vault encryption failed:\n{result.stderr}")
|
"ansible-vault", "encrypt_string",
|
||||||
return result.stdout.strip()
|
value, f"--name={name}",
|
||||||
|
"--vault-password-file", vault_password_file
|
||||||
|
]
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(f"ansible-vault encrypt_string failed:\n{proc.stderr}")
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
def load_yaml_file(path):
|
def load_yaml(path: Path):
|
||||||
"""Load a YAML file or return an empty dict if not found."""
|
if not path.exists():
|
||||||
if path.exists():
|
return {}
|
||||||
with open(path, "r") as f:
|
return yaml.safe_load(path.read_text()) or {}
|
||||||
return yaml.safe_load(f) or {}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def save_yaml_file(path, data):
|
def save_yaml(path: Path, data):
|
||||||
"""Save a dictionary to a YAML file."""
|
path.write_text(yaml.dump(data, sort_keys=False, Dumper=yaml.SafeDumper))
|
||||||
with open(path, "w") as f:
|
|
||||||
yaml.dump(data, f, sort_keys=False)
|
|
||||||
|
|
||||||
def parse_overrides(pairs):
|
def parse_overrides(pairs):
|
||||||
"""Parse key=value overrides into a dictionary."""
|
out = {}
|
||||||
result = {}
|
for p in pairs:
|
||||||
for pair in pairs:
|
if "=" in p:
|
||||||
if "=" not in pair:
|
k, v = p.split("=", 1)
|
||||||
continue
|
out[k.strip()] = v.strip()
|
||||||
k, v = pair.split("=", 1)
|
return out
|
||||||
result[k.strip()] = v.strip()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def load_application_id_from_vars(role_path):
|
def load_application_id(role_path: Path):
|
||||||
"""Read application_id from role's vars/main.yml"""
|
vars_file = role_path / "vars" / "main.yml"
|
||||||
vars_file = Path(role_path) / "vars" / "main.yml"
|
|
||||||
if not vars_file.exists():
|
if not vars_file.exists():
|
||||||
raise FileNotFoundError(f"{vars_file} not found.")
|
raise FileNotFoundError(f"{vars_file} not found")
|
||||||
vars_data = load_yaml_file(vars_file)
|
data = load_yaml(vars_file)
|
||||||
app_id = vars_data.get("application_id")
|
app_id = data.get("application_id")
|
||||||
if not app_id:
|
if not app_id:
|
||||||
raise KeyError(f"'application_id' not found in {vars_file}")
|
raise KeyError(f"'application_id' missing in {vars_file}")
|
||||||
return app_id
|
return app_id
|
||||||
|
|
||||||
def apply_schema_to_inventory(schema, inventory_data, app_id, overrides, vault_password_file, ask_vault_pass):
|
def apply_schema(schema: dict,
|
||||||
"""Merge schema into inventory under applications.{app_id}, encrypting all values."""
|
inventory: dict,
|
||||||
inventory_data.setdefault("applications", {})
|
app_id: str,
|
||||||
applications = inventory_data["applications"]
|
overrides: dict,
|
||||||
|
vault_pw: str):
|
||||||
|
apps = inventory.setdefault("applications", {})
|
||||||
|
target = apps.setdefault(app_id, {})
|
||||||
|
|
||||||
applications.setdefault(app_id, {})
|
def recurse(branch, dest, prefix=""):
|
||||||
|
|
||||||
def process_branch(branch, target, path_prefix=""):
|
|
||||||
for key, meta in branch.items():
|
for key, meta in branch.items():
|
||||||
full_key_path = f"{path_prefix}.{key}" if path_prefix else key
|
full = f"{prefix}.{key}" if prefix else key
|
||||||
if isinstance(meta, dict) and all(k in meta for k in ["description", "algorithm", "validation"]):
|
|
||||||
if key in target:
|
|
||||||
overwrite = prompt(f"Key '{full_key_path}' already exists. Overwrite?", "n").lower() == "y"
|
|
||||||
if not overwrite:
|
|
||||||
continue
|
|
||||||
plain_value = overrides.get(full_key_path, generate_value(meta["algorithm"]))
|
|
||||||
vaulted_value = encrypt_with_vault(plain_value, key, vault_password_file, ask_vault_pass)
|
|
||||||
target[key] = yaml.load(vaulted_value, Loader=yaml.SafeLoader)
|
|
||||||
elif isinstance(meta, dict):
|
|
||||||
target.setdefault(key, {})
|
|
||||||
process_branch(meta, target[key], full_key_path)
|
|
||||||
else:
|
|
||||||
target[key] = meta
|
|
||||||
|
|
||||||
process_branch(schema, applications[app_id])
|
# leaf spec
|
||||||
return inventory_data
|
if isinstance(meta, dict) and all(k in meta for k in ("description","algorithm","validation")):
|
||||||
|
plain = overrides.get(full, generate_value(meta["algorithm"]))
|
||||||
|
snippet = encrypt_with_vault(plain, key, vault_pw)
|
||||||
|
lines = snippet.splitlines()
|
||||||
|
indent = len(lines[1]) - len(lines[1].lstrip())
|
||||||
|
body = "\n".join(line[indent:] for line in lines[1:])
|
||||||
|
dest[key] = VaultScalar(body)
|
||||||
|
|
||||||
|
# nested
|
||||||
|
elif isinstance(meta, dict):
|
||||||
|
sub = dest.setdefault(key, {})
|
||||||
|
recurse(meta, sub, full)
|
||||||
|
|
||||||
|
# passthrough
|
||||||
|
else:
|
||||||
|
dest[key] = meta
|
||||||
|
|
||||||
|
recurse(schema, target)
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
def encrypt_leaves(branch: dict, vault_pw: str):
|
||||||
|
for k, v in list(branch.items()):
|
||||||
|
if isinstance(v, dict):
|
||||||
|
encrypt_leaves(v, vault_pw)
|
||||||
|
else:
|
||||||
|
plain = str(v)
|
||||||
|
if plain.lstrip().startswith("$ANSIBLE_VAULT"):
|
||||||
|
continue
|
||||||
|
snippet = encrypt_with_vault(plain, k, vault_pw)
|
||||||
|
lines = snippet.splitlines()
|
||||||
|
indent = len(lines[1]) - len(lines[1].lstrip())
|
||||||
|
body = "\n".join(line[indent:] for line in lines[1:])
|
||||||
|
branch[k] = VaultScalar(body)
|
||||||
|
|
||||||
|
def encrypt_credentials_branch(node, vault_pw: str):
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for k, v in node.items():
|
||||||
|
if k == "credentials" and isinstance(v, dict):
|
||||||
|
encrypt_leaves(v, vault_pw)
|
||||||
|
else:
|
||||||
|
encrypt_credentials_branch(v, vault_pw)
|
||||||
|
elif isinstance(node, list):
|
||||||
|
for item in node:
|
||||||
|
encrypt_credentials_branch(item, vault_pw)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Generate Vault-encrypted credentials from schema and write to inventory.")
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument("--role-path", help="Path to the Ansible role")
|
description="Selectively vault credentials + become-password in an inventory file"
|
||||||
parser.add_argument("--inventory-file", help="Path to the inventory file to update")
|
)
|
||||||
parser.add_argument("--vault-password-file", help="Path to Ansible Vault password file")
|
parser.add_argument("--role-path", required=True,
|
||||||
parser.add_argument("--ask-vault-pass", action="store_true", help="Prompt for vault password")
|
help="Path to your Ansible role")
|
||||||
parser.add_argument("--set", nargs="*", default=[], help="Override values as key=value")
|
parser.add_argument("--inventory-file", required=True,
|
||||||
|
help="Vaulted host_vars file to update")
|
||||||
|
parser.add_argument("--vault-password-file", required=True,
|
||||||
|
help="Ansible Vault password file")
|
||||||
|
parser.add_argument("--set", nargs="*", default=[],
|
||||||
|
help="Override values as key.subkey=VALUE")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Prompt for missing values
|
role_path = Path(args.role_path)
|
||||||
role_path = Path(args.role_path or prompt("Path to Ansible role", "./roles/docker-<app>"))
|
inv_file = Path(args.inventory_file)
|
||||||
inventory_file = Path(args.inventory_file or prompt("Path to inventory file", "./host_vars/localhost.yml"))
|
vault_pw = args.vault_password_file
|
||||||
|
overrides = parse_overrides(args.set)
|
||||||
|
|
||||||
# Determine application_id
|
app_id = load_application_id(role_path)
|
||||||
app_id = load_application_id_from_vars(role_path)
|
|
||||||
|
|
||||||
# Vault method
|
# 1) decrypt-or-plain-load
|
||||||
if not args.vault_password_file and not args.ask_vault_pass:
|
inventory = decrypt_inventory(inv_file, vault_pw)
|
||||||
print("[?] No Vault password method provided.")
|
|
||||||
print(" 1) Provide path to --vault-password-file")
|
|
||||||
print(" 2) Use interactive prompt (--ask-vault-pass)")
|
|
||||||
choice = prompt("Select method", "1")
|
|
||||||
if choice == "1":
|
|
||||||
args.vault_password_file = prompt("Vault password file", "~/.vault_pass.txt").replace("~", str(Path.home()))
|
|
||||||
else:
|
|
||||||
args.ask_vault_pass = True
|
|
||||||
|
|
||||||
# Load files
|
# 2) merge schema-driven credentials
|
||||||
schema_path = role_path / "meta" / "schema.yml"
|
schema = load_yaml(role_path / "meta" / "schema.yml")
|
||||||
schema_data = load_yaml_file(schema_path)
|
inventory = apply_schema(schema, inventory, app_id, overrides, vault_pw)
|
||||||
inventory_data = load_yaml_file(inventory_file)
|
|
||||||
overrides = parse_overrides(args.set)
|
|
||||||
|
|
||||||
# Apply schema and save
|
# 3) vault leaves under credentials:
|
||||||
updated = apply_schema_to_inventory(
|
encrypt_credentials_branch(inventory, vault_pw)
|
||||||
schema=schema_data,
|
|
||||||
inventory_data=inventory_data,
|
|
||||||
app_id=app_id,
|
|
||||||
overrides=overrides,
|
|
||||||
vault_password_file=args.vault_password_file,
|
|
||||||
ask_vault_pass=args.ask_vault_pass
|
|
||||||
)
|
|
||||||
|
|
||||||
save_yaml_file(inventory_file, updated)
|
# 4) vault top-level become password
|
||||||
print(f"\n✅ Inventory file updated at: {inventory_file}")
|
if "ansible_become_password" in inventory:
|
||||||
|
val = str(inventory["ansible_become_password"])
|
||||||
|
if not val.lstrip().startswith("$ANSIBLE_VAULT"):
|
||||||
|
snippet = encrypt_with_vault(val, "ansible_become_password", vault_pw)
|
||||||
|
lines = snippet.splitlines()
|
||||||
|
indent = len(lines[1]) - len(lines[1].lstrip())
|
||||||
|
body = "\n".join(line[indent:] for line in lines[1:])
|
||||||
|
inventory["ansible_become_password"] = VaultScalar(body)
|
||||||
|
|
||||||
|
# 5) write back YAML
|
||||||
|
save_yaml(inv_file, inventory)
|
||||||
|
print(f"✅ Inventory selectively vaulted → {inv_file}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user