cloud security

Implementing Secrets Management with Vault

This skill covers deploying HashiCorp Vault for centralized secrets management across cloud environments, including dynamic secret generation for databases and cloud providers, transit encryption, PKI certificate management, and Kubernetes integration. It addresses eliminating hardcoded credentials from application code and CI/CD pipelines by implementing short-lived, automatically rotated secrets.

credential-rotationdynamic-secretshashicorp-vaultsecrets-managementzero-trust
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When applications store database passwords, API keys, or certificates in environment variables or config files
  • When migrating from static long-lived credentials to dynamic short-lived secrets
  • When Kubernetes workloads need secure access to database credentials or cloud provider APIs
  • When compliance requirements mandate centralized credential management with audit logging
  • When CI/CD pipelines contain hardcoded secrets that represent supply chain risk

Do not use for AWS-only environments where AWS Secrets Manager suffices without multi-cloud requirements, for application-level encryption logic (though Vault Transit can help), or for identity federation (see managing-cloud-identity-with-okta).

Prerequisites

  • HashiCorp Vault server deployed in HA mode (Consul or Raft storage backend)
  • TLS certificates for Vault listener endpoints
  • Vault Enterprise license for namespaces, Sentinel policies, and replication (optional)
  • Kubernetes cluster with Vault Agent Injector or CSI provider for workload integration

Workflow

Step 1: Deploy Vault in High Availability Mode

Deploy Vault using Integrated Storage (Raft) for HA without external dependencies. Configure TLS, audit logging, and auto-unseal using a cloud KMS.

# vault-config.hcl
storage "raft" {
  path    = "/opt/vault/data"
  node_id = "vault-node-1"
 
  retry_join {
    leader_api_addr = "https://vault-node-2.internal:8200"
  }
  retry_join {
    leader_api_addr = "https://vault-node-3.internal:8200"
  }
}
 
listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_cert_file = "/opt/vault/tls/vault.crt"
  tls_key_file  = "/opt/vault/tls/vault.key"
}
 
seal "awskms" {
  region     = "us-east-1"
  kms_key_id = "alias/vault-unseal-key"
}
 
api_addr      = "https://vault-node-1.internal:8200"
cluster_addr  = "https://vault-node-1.internal:8201"
 
telemetry {
  prometheus_retention_time = "30s"
  disable_hostname         = true
}
# Initialize Vault
vault operator init -key-shares=5 -key-threshold=3
 
# Enable audit logging
vault audit enable file file_path=/var/log/vault/audit.log
 
# Enable syslog audit for SIEM integration
vault audit enable syslog tag="vault" facility="AUTH"

Step 2: Configure Authentication Methods

Enable authentication backends for human operators, applications, and CI/CD pipelines. Use AppRole for machine authentication and OIDC for human access.

# Enable OIDC auth for human users via Okta
vault auth enable oidc
vault write auth/oidc/config \
  oidc_discovery_url="https://company.okta.com/oauth2/default" \
  oidc_client_id="vault-client-id" \
  oidc_client_secret="vault-client-secret" \
  default_role="default"
 
# Enable AppRole for application authentication
vault auth enable approle
vault write auth/approle/role/web-app \
  secret_id_ttl=10m \
  token_num_uses=10 \
  token_ttl=20m \
  token_max_ttl=30m \
  secret_id_num_uses=1 \
  token_policies="web-app-policy"
 
# Enable Kubernetes auth for pod-based access
vault auth enable kubernetes
vault write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc:443" \
  token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \
  kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

Step 3: Enable Dynamic Secret Engines

Configure database secret engines to generate short-lived credentials on demand. Each credential set has a TTL and is automatically revoked when it expires.

# Enable database secrets engine for PostgreSQL
vault secrets enable database
vault write database/config/production-db \
  plugin_name=postgresql-database-plugin \
  allowed_roles="readonly,readwrite" \
  connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/production?sslmode=require" \
  username="vault_admin" \
  password="initial-password"
 
# Rotate the root credentials so Vault manages them exclusively
vault write -force database/rotate-root/production-db
 
# Create a readonly role with 1-hour TTL
vault write database/roles/readonly \
  db_name=production-db \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  revocation_statements="REVOKE ALL ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"; DROP ROLE IF EXISTS \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"
 
# Enable AWS secrets engine for dynamic IAM credentials
vault secrets enable aws
vault write aws/config/root \
  access_key=AKIAEXAMPLE \
  secret_key=secretkey \
  region=us-east-1
 
vault write aws/roles/deploy-role \
  credential_type=iam_user \
  policy_document=@deploy-policy.json \
  default_sts_ttl=3600

Step 4: Integrate with Kubernetes Workloads

Use the Vault Agent Injector or CSI Provider to deliver secrets to pods without application code changes. Secrets are rendered as files in a shared volume.

# Kubernetes deployment with Vault Agent Injector annotations
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "web-app"
        vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/readonly"
        vault.hashicorp.com/agent-inject-template-db-creds: |
          {{- with secret "database/creds/readonly" -}}
          export DB_USERNAME="{{ .Data.username }}"
          export DB_PASSWORD="{{ .Data.password }}"
          {{- end }}
    spec:
      serviceAccountName: web-app
      containers:
        - name: web-app
          image: company/web-app:v2.1
          command: ["/bin/sh", "-c", "source /vault/secrets/db-creds && ./start.sh"]

Step 5: Implement Transit Encryption and PKI

Use the Transit secrets engine for application-level encryption without managing keys in application code. Deploy the PKI engine for automatic TLS certificate management.

# Enable Transit engine for encryption as a service
vault secrets enable transit
vault write -f transit/keys/payment-data type=aes256-gcm96
 
# Encrypt sensitive data
vault write transit/encrypt/payment-data \
  plaintext=$(echo "card-number-4111-1111-1111-1111" | base64)
 
# Enable PKI for internal certificate management
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
 
# Generate root CA
vault write pki/root/generate/internal \
  common_name="Internal Root CA" \
  ttl=87600h
 
# Configure intermediate CA for issuing certificates
vault secrets enable -path=pki_int pki
vault write pki_int/intermediate/generate/internal \
  common_name="Internal Intermediate CA" \
  ttl=43800h
 
# Create a role for issuing certificates
vault write pki_int/roles/internal-services \
  allowed_domains="internal.company.com" \
  allow_subdomains=true \
  max_ttl=720h

Step 6: Establish Policies and Audit Trail

Define fine-grained ACL policies following least privilege. Enable comprehensive audit logging for all secret access and administrative operations.

# web-app-policy.hcl
path "database/creds/readonly" {
  capabilities = ["read"]
}
 
path "transit/encrypt/payment-data" {
  capabilities = ["update"]
}
 
path "transit/decrypt/payment-data" {
  capabilities = ["update"]
}
 
path "secret/data/web-app/*" {
  capabilities = ["read", "list"]
}
 
# Deny access to admin paths
path "sys/*" {
  capabilities = ["deny"]
}
# Apply the policy
vault policy write web-app-policy web-app-policy.hcl
 
# Verify audit log captures all operations
vault audit list -detailed

Key Concepts

Term Definition
Dynamic Secrets Credentials generated on-demand with automatic expiration and revocation, eliminating long-lived static credentials
Secret Engine Vault component that stores, generates, or encrypts data; includes KV, database, AWS, PKI, and Transit engines
Auto-Unseal Cloud KMS-based mechanism that automatically unseals Vault nodes on restart without manual key entry
AppRole Machine-oriented authentication method using Role ID and Secret ID for application and CI/CD pipeline access
Transit Engine Encryption-as-a-service engine that handles cryptographic operations without exposing encryption keys to applications
Lease Time-bound credential with a TTL that Vault automatically revokes on expiration unless renewed
Namespace Vault Enterprise feature providing tenant isolation with separate auth, secrets, and policy management
Response Wrapping Technique that wraps secret responses in a single-use token to prevent man-in-the-middle exposure during delivery

Tools & Systems

  • HashiCorp Vault: Core secrets management platform providing dynamic secrets, encryption, and identity-based access
  • Vault Agent Injector: Kubernetes mutating webhook that automatically injects Vault secrets into pod volumes via sidecar containers
  • Vault CSI Provider: Kubernetes CSI driver that mounts Vault secrets directly into pod volumes without sidecar containers
  • consul-template: Template rendering daemon that watches Vault secrets and re-renders configuration files when secrets change
  • Vault Radar: Secret scanning tool that detects hardcoded credentials in source code, CI/CD pipelines, and cloud configurations

Common Scenarios

Scenario: Eliminating Hardcoded Database Credentials from CI/CD Pipeline

Context: A DevOps team stores PostgreSQL credentials in GitHub Actions secrets and Jenkins credential stores. The same credentials are shared across staging and production environments with no rotation for 18 months.

Approach:

  1. Deploy Vault with AppRole auth enabled for CI/CD systems
  2. Configure the database secrets engine with separate roles for staging (readwrite, 2h TTL) and production (readonly, 1h TTL)
  3. Create separate Vault policies for each pipeline stage restricting access to the appropriate database role
  4. Update GitHub Actions workflows to authenticate via AppRole and request dynamic credentials at the start of each job
  5. Rotate the static PostgreSQL credentials and hand root access to Vault exclusively
  6. Enable audit logging to track every credential request with pipeline job metadata

Pitfalls: Failing to rotate the original static credentials after Vault migration leaves the old credentials valid. Setting TTLs too short causes credential expiry mid-deployment for long-running jobs.

Output Format

Vault Secrets Management Audit Report
=======================================
Vault Cluster: vault.internal.company.com
Version: 1.18.1 Enterprise
HA Mode: Raft (3 nodes)
Seal Type: AWS KMS Auto-Unseal
Report Date: 2025-02-23
 
SECRET ENGINES:
  database/         PostgreSQL dynamic creds   Leases Active: 47
  aws/              Dynamic IAM credentials    Leases Active: 12
  transit/          Encryption as a service    Keys: 8
  pki/              Root CA                    Certs Issued: 0
  pki_int/          Intermediate CA            Certs Issued: 234
  secret/           KV v2 static secrets       Versions: 1,892
 
AUTH METHODS:
  oidc/             Okta SSO for humans        Active Tokens: 23
  approle/          CI/CD pipelines            Active Tokens: 156
  kubernetes/       Pod-based auth             Active Tokens: 89
 
AUDIT FINDINGS:
  [WARN] 3 AppRole secret_id_num_uses set to 0 (unlimited)
  [WARN] 12 KV secrets not accessed in 90+ days (potential orphans)
  [PASS] All dynamic secret TTLs under 24 hours
  [PASS] Audit logging enabled on all nodes
  [PASS] Root token revoked after initial setup
 
CREDENTIAL HYGIENE:
  Static Secrets (KV): 234
  Dynamic Secrets Active: 59
  Average Lease TTL: 2.3 hours
  Secrets Rotated This Month: 12,456
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md4.1 KB

API Reference: HashiCorp Vault Secrets Management

Libraries Used

Library Purpose
hvac Official Python client for HashiCorp Vault API
requests HTTP fallback for direct Vault REST calls
json Parse Vault JSON responses
os Read VAULT_ADDR and VAULT_TOKEN environment variables

Installation

pip install hvac requests

Authentication

Token Authentication

import hvac
 
client = hvac.Client(
    url=os.environ.get("VAULT_ADDR", "https://127.0.0.1:8200"),
    token=os.environ.get("VAULT_TOKEN"),
)
assert client.is_authenticated()

AppRole Authentication

client = hvac.Client(url=os.environ["VAULT_ADDR"])
resp = client.auth.approle.login(
    role_id=os.environ["VAULT_ROLE_ID"],
    secret_id=os.environ["VAULT_SECRET_ID"],
)
client.token = resp["auth"]["client_token"]

Kubernetes Authentication

with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as f:
    jwt = f.read()
client.auth.kubernetes.login(role="my-role", jwt=jwt)

Core API — KV Secrets Engine v2

Write a Secret

client.secrets.kv.v2.create_or_update_secret(
    path="myapp/database",
    secret={"username": "admin", "password": "s3cure!"},
    mount_point="secret",
)

Read a Secret

resp = client.secrets.kv.v2.read_secret_version(
    path="myapp/database",
    mount_point="secret",
)
data = resp["data"]["data"]  # {"username": "admin", "password": "s3cure!"}

List Secrets

resp = client.secrets.kv.v2.list_secrets(path="myapp/", mount_point="secret")
keys = resp["data"]["keys"]  # ["database", "api-keys", ...]

Delete a Secret

client.secrets.kv.v2.delete_metadata_and_all_versions(
    path="myapp/database",
    mount_point="secret",
)

System Backend — Audit and Health

Check Seal Status

status = client.sys.read_seal_status()
# {"sealed": False, "t": 3, "n": 5, "progress": 0}

List Auth Methods

methods = client.sys.list_auth_methods()
# {"token/": {...}, "approle/": {...}, ...}

List Enabled Secrets Engines

engines = client.sys.list_mounted_secrets_engines()

Enable Audit Device

client.sys.enable_audit_device(
    device_type="file",
    options={"file_path": "/var/log/vault_audit.log"},
)

Transit Secrets Engine — Encryption as a Service

Encrypt Data

import base64
plaintext_b64 = base64.b64encode(b"sensitive-data").decode()
resp = client.secrets.transit.encrypt_data(
    name="my-key",
    plaintext=plaintext_b64,
)
ciphertext = resp["data"]["ciphertext"]  # "vault:v1:..."

Decrypt Data

resp = client.secrets.transit.decrypt_data(
    name="my-key",
    ciphertext=ciphertext,
)
plaintext = base64.b64decode(resp["data"]["plaintext"])

REST API Endpoints (Direct)

Method Endpoint Description
GET /v1/sys/health Health check and seal status
GET /v1/sys/seal-status Detailed seal status
POST /v1/auth/token/create Create new token
GET /v1/secret/data/{path} Read KV v2 secret
POST /v1/secret/data/{path} Write KV v2 secret
LIST /v1/secret/metadata/{path} List secrets at path
DELETE /v1/secret/metadata/{path} Permanently delete secret
POST /v1/transit/encrypt/{key} Encrypt with transit engine
POST /v1/transit/decrypt/{key} Decrypt with transit engine

Error Handling

from hvac.exceptions import Forbidden, InvalidPath, VaultError
 
try:
    secret = client.secrets.kv.v2.read_secret_version(path="missing")
except InvalidPath:
    print("Secret path does not exist")
except Forbidden:
    print("Insufficient permissions — check Vault policy")
except VaultError as e:
    print(f"Vault error: {e}")

Output Format

{
  "request_id": "abc-123",
  "lease_id": "",
  "renewable": false,
  "data": {
    "data": {"username": "admin", "password": "s3cure!"},
    "metadata": {
      "created_time": "2025-01-15T10:30:00.000Z",
      "version": 3,
      "destroyed": false
    }
  }
}

Scripts 1

agent.py10.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""HashiCorp Vault secrets management agent.

Manages secrets lifecycle using the HashiCorp Vault KV v2 secrets engine
via the hvac Python library. Supports reading, writing, listing, deleting
secrets, and auditing secret access patterns.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone

try:
    import hvac
except ImportError:
    print("[!] 'hvac' library required: pip install hvac", file=sys.stderr)
    sys.exit(1)


def get_vault_client(addr=None, token=None, namespace=None):
    """Create and authenticate a Vault client."""
    vault_addr = addr or os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")
    vault_token = token or os.environ.get("VAULT_TOKEN", "")
    vault_ns = namespace or os.environ.get("VAULT_NAMESPACE")

    if not vault_token:
        print("[!] Set VAULT_TOKEN env var or use --token", file=sys.stderr)
        sys.exit(1)

    client = hvac.Client(url=vault_addr, token=vault_token, namespace=vault_ns)
    if not client.is_authenticated():
        print("[!] Vault authentication failed", file=sys.stderr)
        sys.exit(1)
    print(f"[+] Connected to Vault at {vault_addr}")
    return client


def write_secret(client, path, data, mount_point="secret"):
    """Write or update a secret in KV v2."""
    print(f"[*] Writing secret to {mount_point}/{path}")
    response = client.secrets.kv.v2.create_or_update_secret(
        path=path, secret=data, mount_point=mount_point,
    )
    version = response.get("data", {}).get("version", "unknown")
    print(f"[+] Secret written (version: {version})")
    return {"path": path, "version": version, "action": "write"}


def read_secret(client, path, mount_point="secret", version=None):
    """Read a secret from KV v2."""
    print(f"[*] Reading secret from {mount_point}/{path}")
    kwargs = {"path": path, "mount_point": mount_point}
    if version:
        kwargs["version"] = version
    response = client.secrets.kv.v2.read_secret_version(**kwargs)
    secret_data = response.get("data", {}).get("data", {})
    metadata = response.get("data", {}).get("metadata", {})
    print(f"[+] Secret read (version: {metadata.get('version', '?')}, "
          f"created: {metadata.get('created_time', 'unknown')})")
    masked = {k: v[:3] + "***" if isinstance(v, str) and len(v) > 3 else "***"
              for k, v in secret_data.items()}
    return {
        "path": path,
        "keys": list(secret_data.keys()),
        "masked_values": masked,
        "version": metadata.get("version"),
        "created_time": metadata.get("created_time"),
        "deletion_time": metadata.get("deletion_time"),
        "destroyed": metadata.get("destroyed"),
    }


def list_secrets(client, path="", mount_point="secret"):
    """List secret paths under a given prefix."""
    print(f"[*] Listing secrets under {mount_point}/{path}")
    try:
        response = client.secrets.kv.v2.list_secrets(
            path=path, mount_point=mount_point
        )
        keys = response.get("data", {}).get("keys", [])
        print(f"[+] Found {len(keys)} entries")
        for key in keys:
            marker = "/" if key.endswith("/") else " "
            print(f"    {marker} {key}")
        return {"path": path, "entries": keys, "count": len(keys)}
    except hvac.exceptions.InvalidPath:
        print(f"[*] No secrets found at {mount_point}/{path}")
        return {"path": path, "entries": [], "count": 0}


def delete_secret(client, path, mount_point="secret", versions=None, destroy=False):
    """Delete or destroy a secret."""
    if destroy and versions:
        print(f"[*] Permanently destroying versions {versions} at {mount_point}/{path}")
        client.secrets.kv.v2.destroy_secret_versions(
            path=path, versions=versions, mount_point=mount_point
        )
        print(f"[+] Versions {versions} permanently destroyed")
        return {"path": path, "action": "destroy", "versions": versions}
    elif versions:
        print(f"[*] Soft-deleting versions {versions} at {mount_point}/{path}")
        client.secrets.kv.v2.delete_secret_versions(
            path=path, versions=versions, mount_point=mount_point
        )
        print(f"[+] Versions {versions} soft-deleted (recoverable)")
        return {"path": path, "action": "soft_delete", "versions": versions}
    else:
        print(f"[*] Deleting latest version at {mount_point}/{path}")
        client.secrets.kv.v2.delete_latest_version_of_secret(
            path=path, mount_point=mount_point
        )
        print(f"[+] Latest version deleted")
        return {"path": path, "action": "delete_latest"}


def read_metadata(client, path, mount_point="secret"):
    """Read secret metadata including version history."""
    print(f"[*] Reading metadata for {mount_point}/{path}")
    response = client.secrets.kv.v2.read_secret_metadata(
        path=path, mount_point=mount_point
    )
    meta = response.get("data", {})
    versions = meta.get("versions", {})
    print(f"[+] {len(versions)} version(s), max_versions: {meta.get('max_versions', 0)}")
    version_info = []
    for ver_num, ver_data in sorted(versions.items(), key=lambda x: int(x[0])):
        version_info.append({
            "version": int(ver_num),
            "created_time": ver_data.get("created_time"),
            "deletion_time": ver_data.get("deletion_time"),
            "destroyed": ver_data.get("destroyed", False),
        })
    return {
        "path": path,
        "current_version": meta.get("current_version"),
        "max_versions": meta.get("max_versions"),
        "cas_required": meta.get("cas_required"),
        "created_time": meta.get("created_time"),
        "updated_time": meta.get("updated_time"),
        "versions": version_info,
    }


def audit_secrets(client, mount_point="secret", path_prefix=""):
    """Audit all secrets under a path, reporting metadata and age."""
    print(f"[*] Auditing secrets under {mount_point}/{path_prefix}")
    audit_results = []

    def _recurse(current_path):
        try:
            resp = client.secrets.kv.v2.list_secrets(
                path=current_path, mount_point=mount_point
            )
        except hvac.exceptions.InvalidPath:
            return
        keys = resp.get("data", {}).get("keys", [])
        for key in keys:
            full_path = f"{current_path}{key}" if current_path else key
            if key.endswith("/"):
                _recurse(full_path)
            else:
                try:
                    meta_resp = client.secrets.kv.v2.read_secret_metadata(
                        path=full_path, mount_point=mount_point
                    )
                    meta = meta_resp.get("data", {})
                    audit_results.append({
                        "path": full_path,
                        "current_version": meta.get("current_version"),
                        "created_time": meta.get("created_time"),
                        "updated_time": meta.get("updated_time"),
                        "version_count": len(meta.get("versions", {})),
                    })
                except Exception as e:
                    audit_results.append({"path": full_path, "error": str(e)})

    _recurse(path_prefix)
    print(f"[+] Audited {len(audit_results)} secret(s)")
    for item in audit_results:
        if "error" in item:
            print(f"    [ERR] {item['path']}: {item['error']}")
        else:
            print(f"    {item['path']:40s} v{item['current_version']} "
                  f"(updated: {str(item.get('updated_time', 'N/A'))[:19]})")
    return audit_results


def main():
    parser = argparse.ArgumentParser(
        description="HashiCorp Vault secrets management agent"
    )
    sub = parser.add_subparsers(dest="command", help="Action to perform")

    p_write = sub.add_parser("write", help="Write a secret")
    p_write.add_argument("--path", required=True, help="Secret path")
    p_write.add_argument("--data", required=True, help='JSON data')
    p_write.add_argument("--mount", default="secret", help="KV mount point")

    p_read = sub.add_parser("read", help="Read a secret")
    p_read.add_argument("--path", required=True)
    p_read.add_argument("--version", type=int, help="Specific version")
    p_read.add_argument("--mount", default="secret")

    p_list = sub.add_parser("list", help="List secrets")
    p_list.add_argument("--path", default="")
    p_list.add_argument("--mount", default="secret")

    p_del = sub.add_parser("delete", help="Delete a secret")
    p_del.add_argument("--path", required=True)
    p_del.add_argument("--versions", type=int, nargs="+")
    p_del.add_argument("--destroy", action="store_true")
    p_del.add_argument("--mount", default="secret")

    p_meta = sub.add_parser("metadata", help="Read secret metadata")
    p_meta.add_argument("--path", required=True)
    p_meta.add_argument("--mount", default="secret")

    p_audit = sub.add_parser("audit", help="Audit all secrets")
    p_audit.add_argument("--path", default="")
    p_audit.add_argument("--mount", default="secret")

    parser.add_argument("--addr", help="Vault address (or VAULT_ADDR env)")
    parser.add_argument("--token", help="Vault token (or VAULT_TOKEN env)")
    parser.add_argument("--namespace", help="Vault namespace")
    parser.add_argument("--output", "-o", help="Output JSON report")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    client = get_vault_client(args.addr, args.token, args.namespace)

    if args.command == "write":
        secret_data = json.loads(args.data)
        result = write_secret(client, args.path, secret_data, args.mount)
    elif args.command == "read":
        result = read_secret(client, args.path, args.mount,
                             getattr(args, "version", None))
    elif args.command == "list":
        result = list_secrets(client, args.path, args.mount)
    elif args.command == "delete":
        result = delete_secret(client, args.path, args.mount,
                               getattr(args, "versions", None),
                               getattr(args, "destroy", False))
    elif args.command == "metadata":
        result = read_metadata(client, args.path, args.mount)
    elif args.command == "audit":
        result = audit_secrets(client, args.mount, args.path)
    else:
        parser.print_help()
        sys.exit(1)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "HashiCorp Vault",
        "command": args.command,
        "result": result,
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
Keep exploring