diff --git a/servers/gcp/vault-auth-gcp.tf b/servers/gcp/vault-auth-gcp.tf new file mode 100644 index 0000000..8043e2d --- /dev/null +++ b/servers/gcp/vault-auth-gcp.tf @@ -0,0 +1,48 @@ +# ── Vault GCP IAM Auth Method — GCP IAM for Cloud Run services ─────────────── +# +# This file defines the GCP Secret Manager secret that holds the Vault GCP +# auth configuration script, and the Vault policies for each service identity. +# +# IMPORTANT: The Vault Terraform provider is NOT configured in this workspace. +# The actual Vault auth method configuration is performed by running: +# +# servers/gcp/vault/configure-vault-auth.sh +# +# That script uses the `vault` CLI and must be run AFTER: +# 1. The vault-nodes are up and initialized (vault operator init on node-1) +# 2. VAULT_ADDR and VAULT_TOKEN are set in the environment +# +# Why no Vault Terraform provider here: +# - Adding the provider requires a working Vault cluster at `terraform init` +# - The cluster doesn't exist yet when this plan is first applied +# - The provider would encode the root token in TF state (undesirable) +# - A shell script run once post-bootstrap is simpler and more auditable +# +# If you want to add the Vault provider in a future iteration, the script +# below maps directly to `vault_auth_backend`, `vault_policy`, and +# `vault_gcp_auth_backend_role` resources. + +# ── GCP Secret Manager secret for Vault auth init script ───────────────────── +# Store the post-init script here so it can be fetched from any node via +# gcloud secrets versions access — no file transfer needed. + +resource "google_secret_manager_secret" "vault_auth_init_script" { + secret_id = "vault-auth-init-script" + project = var.project_id + + replication { + auto {} + } +} + +# ── Service account SA email data sources (for use in the configure script) ── + +# The script uses these SA emails — surfaced here as outputs for convenience + +output "vault_auth_sa_emails" { + description = "Service account emails for Vault GCP auth roles — used in configure-vault-auth.sh" + value = { + marketing = google_service_account.marketing.email + soma = google_service_account.soma.email + } +} diff --git a/servers/gcp/vault-nodes.tf b/servers/gcp/vault-nodes.tf new file mode 100644 index 0000000..601de00 --- /dev/null +++ b/servers/gcp/vault-nodes.tf @@ -0,0 +1,357 @@ +# ── Vault HA Cluster — GCE-based Raft ──────────────────────────────────────── +# +# Three GCE e2-small VMs across us-central1-{a,b,c} running HashiCorp Vault +# in Raft HA mode. Auto-unseal via the existing GCP KMS key (vault-kms.tf). +# +# Architecture: +# - vault-node-1 (us-central1-a) — bootstrapped first, others join via retry_join +# - vault-node-2 (us-central1-b) +# - vault-node-3 (us-central1-c) +# - Each VM gets the vault-node SA attached for KMS auto-unseal + ADC +# - Internal traffic: VMs talk Raft over port 8201 on GCP internal IPs +# - External access: HTTPS regional LB → port 8200 +# (vault.neuralplatform.ai Cloudflare DNS → GCP LB IP) +# +# Ops SSH: +# gcloud compute ssh vault-node-1 --zone=us-central1-a --tunnel-through-iap +# +# After first boot, initialize Vault on node-1: +# vault operator init (save the root token + recovery keys securely) +# Nodes 2 and 3 auto-join via retry_join once they boot. + +locals { + vault_version = "1.19.2" + + vault_nodes = { + "vault-node-1" = { zone = "us-central1-a", node_id = "vault-node-1" } + "vault-node-2" = { zone = "us-central1-b", node_id = "vault-node-2" } + "vault-node-3" = { zone = "us-central1-c", node_id = "vault-node-3" } + } +} + +# ── Service Account for Vault nodes ────────────────────────────────────────── +# Reuses the vault-unseal SA from vault-kms.tf for KMS access. +# Additional roles: logging + metrics from ops agent. + +resource "google_project_iam_member" "vault_node_log_writer" { + project = var.project_id + role = "roles/logging.logWriter" + member = "serviceAccount:${google_service_account.vault_unseal.email}" +} + +resource "google_project_iam_member" "vault_node_metric_writer" { + project = var.project_id + role = "roles/monitoring.metricWriter" + member = "serviceAccount:${google_service_account.vault_unseal.email}" +} + +# Allow Vault nodes to read their own instance metadata (needed for GCP auth method later) +resource "google_project_iam_member" "vault_node_compute_viewer" { + project = var.project_id + role = "roles/compute.viewer" + member = "serviceAccount:${google_service_account.vault_unseal.email}" +} + +# ── Startup script staged in GCS ───────────────────────────────────────────── +# Stored in the existing runner-assets bucket (reuse infrastructure). +# The script installs Vault, writes the config, and starts the systemd unit. + +resource "google_storage_bucket_object" "vault_startup" { + name = "vault/startup.sh" + bucket = google_storage_bucket.runner_assets.name + source = "${path.module}/vault/startup.sh" + + metadata = { + sha256 = filesha256("${path.module}/vault/startup.sh") + } +} + +resource "google_storage_bucket_iam_member" "vault_node_bucket_read" { + bucket = google_storage_bucket.runner_assets.name + role = "roles/storage.objectViewer" + member = "serviceAccount:${google_service_account.vault_unseal.email}" +} + +# ── Vault node VMs ──────────────────────────────────────────────────────────── + +resource "google_compute_instance" "vault_node" { + for_each = local.vault_nodes + name = each.key + machine_type = "e2-small" + zone = each.value.zone + project = var.project_id + + tags = ["vault-node", "allow-iap-ssh"] + + boot_disk { + initialize_params { + image = "projects/debian-cloud/global/images/family/debian-12" + size = 20 + type = "pd-balanced" + } + } + + # Separate persistent disk for Raft data — survives VM recreation + attached_disk { + source = google_compute_disk.vault_data[each.key].self_link + device_name = "vault-data" + mode = "READ_WRITE" + } + + network_interface { + network = "default" + # No external IP — accessed via IAP SSH and the internal LB + # Vault API is published externally via the regional LB below + access_config {} + } + + service_account { + email = google_service_account.vault_unseal.email + scopes = ["cloud-platform"] + } + + metadata = { + # Pull and execute the real startup script from GCS + startup-script = <<-EOT + #!/usr/bin/env bash + set -euxo pipefail + apt-get update -y + apt-get install -y curl ca-certificates apt-transport-https gnupg google-cloud-cli + gsutil cat gs://${google_storage_bucket.runner_assets.name}/${google_storage_bucket_object.vault_startup.name} \ + > /tmp/vault-startup.sh + chmod +x /tmp/vault-startup.sh + VAULT_NODE_ID="${each.value.node_id}" \ + VAULT_VERSION="${local.vault_version}" \ + /tmp/vault-startup.sh + EOT + + enable-oslogin = "TRUE" + } + + allow_stopping_for_update = true + + depends_on = [ + google_compute_disk.vault_data, + google_storage_bucket_object.vault_startup, + google_storage_bucket_iam_member.vault_node_bucket_read, + ] +} + +# ── Persistent data disks ───────────────────────────────────────────────────── +# 10 GiB per node. Kept separate from the boot disk so Raft data +# survives a full VM deletion and recreation. + +resource "google_compute_disk" "vault_data" { + for_each = local.vault_nodes + name = "${each.key}-data" + zone = each.value.zone + project = var.project_id + type = "pd-ssd" + size = 10 + + labels = { + managed-by = "terraform" + service = "vault" + node = each.key + } + + lifecycle { + prevent_destroy = true + } +} + +# ── Firewall rules ──────────────────────────────────────────────────────────── + +# IAP SSH — ops access without a public SSH port +resource "google_compute_firewall" "vault_iap_ssh" { + name = "vault-iap-ssh" + network = "default" + project = var.project_id + direction = "INGRESS" + priority = 1000 + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_ranges = ["35.235.240.0/20"] # GCP IAP CIDR + target_tags = ["vault-node"] +} + +# Raft cluster traffic — node-to-node port 8201 (internal only) +resource "google_compute_firewall" "vault_raft" { + name = "vault-raft-internal" + network = "default" + project = var.project_id + direction = "INGRESS" + priority = 1000 + + allow { + protocol = "tcp" + ports = ["8201"] + } + + source_tags = ["vault-node"] + target_tags = ["vault-node"] +} + +# Vault API — allow from the GCP health check ranges and the LB +resource "google_compute_firewall" "vault_api" { + name = "vault-api-from-lb" + network = "default" + project = var.project_id + direction = "INGRESS" + priority = 1000 + + allow { + protocol = "tcp" + ports = ["8200"] + } + + # GCP health check ranges (130.211.0.0/22, 35.191.0.0/16) + # and RFC1918 for any internal service access + source_ranges = ["130.211.0.0/22", "35.191.0.0/16", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] + target_tags = ["vault-node"] +} + +# ── Instance Groups (one unmanaged group per zone for the LB) ───────────────── + +resource "google_compute_instance_group" "vault" { + for_each = local.vault_nodes + name = "vault-${each.key}" + zone = each.value.zone + project = var.project_id + instances = [google_compute_instance.vault_node[each.key].self_link] + + named_port { + name = "vault-api" + port = 8200 + } +} + +# ── Regional HTTPS LB for vault.neuralplatform.ai ───────────────────────────── +# We use a global external HTTPS LB (same scheme as the prod marketing LB) +# so we can attach a Google-managed cert for vault.neuralplatform.ai. +# +# TLS is terminated at the LB. Vault listens on plain 8200 internally. +# The Cloudflare DNS A record for vault.neuralplatform.ai (neuralplatform zone) +# must point to vault_lb_ip output below — add it in Cloudflare dashboard +# or in the legion Terraform if you bring that zone under TF management. + +resource "google_compute_global_address" "vault" { + name = "vault-ip" + project = var.project_id +} + +resource "google_compute_managed_ssl_certificate" "vault" { + name = "vault-cert" + project = var.project_id + + managed { + domains = ["vault.neuralplatform.ai"] + } +} + +resource "google_compute_health_check" "vault" { + name = "vault-health" + project = var.project_id + + # Vault listens on plain HTTP (tls_disable = true) — TLS is terminated at the LB + http_health_check { + port = 8200 + request_path = "/v1/sys/health?standbyok=true&sealedok=true&uninitcode=200" + } + + check_interval_sec = 15 + timeout_sec = 5 + healthy_threshold = 2 + unhealthy_threshold = 3 +} + +resource "google_compute_backend_service" "vault" { + name = "vault-backend" + project = var.project_id + load_balancing_scheme = "EXTERNAL_MANAGED" + protocol = "HTTP" # Vault serves plain HTTP; TLS terminates at the LB + timeout_sec = 30 + port_name = "vault-api" + + health_checks = [google_compute_health_check.vault.self_link] + + dynamic "backend" { + for_each = local.vault_nodes + content { + group = google_compute_instance_group.vault[backend.key].self_link + balancing_mode = "UTILIZATION" + max_utilization = 0.8 + } + } + + log_config { + enable = true + sample_rate = 1.0 + } +} + +resource "google_compute_url_map" "vault" { + name = "vault-urlmap" + project = var.project_id + default_service = google_compute_backend_service.vault.self_link +} + +resource "google_compute_target_https_proxy" "vault" { + name = "vault-https-proxy" + project = var.project_id + url_map = google_compute_url_map.vault.self_link + ssl_certificates = [google_compute_managed_ssl_certificate.vault.self_link] +} + +resource "google_compute_target_http_proxy" "vault_redirect" { + name = "vault-http-proxy" + project = var.project_id + url_map = google_compute_url_map.vault_redirect.self_link +} + +resource "google_compute_url_map" "vault_redirect" { + name = "vault-urlmap-http-redirect" + project = var.project_id + + default_url_redirect { + https_redirect = true + redirect_response_code = "MOVED_PERMANENTLY_DEFAULT" + strip_query = false + } +} + +resource "google_compute_global_forwarding_rule" "vault_https" { + name = "vault-fwd-https" + project = var.project_id + target = google_compute_target_https_proxy.vault.self_link + ip_address = google_compute_global_address.vault.address + port_range = "443" + load_balancing_scheme = "EXTERNAL_MANAGED" +} + +resource "google_compute_global_forwarding_rule" "vault_http" { + name = "vault-fwd-http" + project = var.project_id + target = google_compute_target_http_proxy.vault_redirect.self_link + ip_address = google_compute_global_address.vault.address + port_range = "80" + load_balancing_scheme = "EXTERNAL_MANAGED" +} + +# ── Outputs ─────────────────────────────────────────────────────────────────── + +output "vault_lb_ip" { + description = "Global IP for vault.neuralplatform.ai — set as DNS A record in Cloudflare (neuralplatform.ai zone)" + value = google_compute_global_address.vault.address +} + +output "vault_node_zones" { + description = "Zone placement of each Vault node" + value = { + for k, v in local.vault_nodes : k => v.zone + } +} diff --git a/servers/gcp/vault/configure-vault-auth.sh b/servers/gcp/vault/configure-vault-auth.sh new file mode 100644 index 0000000..d288e0b --- /dev/null +++ b/servers/gcp/vault/configure-vault-auth.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# vault/configure-vault-auth.sh +# +# Configures Vault GCP IAM auth method and access policies for Cloud Run services. +# +# Run this ONCE after the Vault cluster is initialized and unsealed: +# +# export VAULT_ADDR=https://vault.neuralplatform.ai +# export VAULT_TOKEN= +# bash servers/gcp/vault/configure-vault-auth.sh +# +# What this script does: +# 1. Enables the GCP auth method at auth/gcp +# 2. Creates Vault policies for marketing and soma +# 3. Creates GCP IAM auth roles bound to each service's Cloud Run SA +# 4. (Optional) Creates the kv-v2 secret engines at secret/ if not present +# +# After running this script, Cloud Run services can authenticate to Vault using +# their GCP service account identity — no long-lived tokens needed. +# +# Auth flow for Cloud Run: +# 1. Service calls metadata server to get an OIDC token for its SA +# 2. POST /v1/auth/gcp/login with role= and jwt= +# 3. Vault validates the token against GCP IAM, returns a Vault token +# 4. Service uses the Vault token to read secrets +# +# Vault CLI docs: +# https://developer.hashicorp.com/vault/docs/auth/gcp + +set -euo pipefail + +: "${VAULT_ADDR:?Set VAULT_ADDR to the Vault cluster address}" +: "${VAULT_TOKEN:?Set VAULT_TOKEN to a root or admin token}" + +GCP_PROJECT="neuron-785695" + +# SA emails from terraform outputs (vault-auth-gcp.tf) +MARKETING_SA="neuron-marketing-sa@${GCP_PROJECT}.iam.gserviceaccount.com" +SOMA_SA="neuron-soma-sa@${GCP_PROJECT}.iam.gserviceaccount.com" + +echo "=== Vault GCP Auth Bootstrap ===" +echo "VAULT_ADDR: ${VAULT_ADDR}" +echo "GCP Project: ${GCP_PROJECT}" +echo "" + +# ── Step 1: Enable kv-v2 secret engine ─────────────────────────────────────── +echo "--- Enabling kv-v2 secret engine at secret/ ---" +if vault secrets list | grep -q "^secret/"; then + echo "secret/ already enabled — skipping" +else + vault secrets enable -path=secret kv-v2 + echo "Enabled secret/ (kv-v2)" +fi + +# ── Step 2: Enable GCP auth method ──────────────────────────────────────────── +echo "" +echo "--- Enabling GCP auth method ---" +if vault auth list | grep -q "^gcp/"; then + echo "gcp/ auth already enabled — skipping" +else + vault auth enable gcp + echo "Enabled gcp/ auth method" +fi + +# Configure the auth method to trust the GCP project +vault write auth/gcp/config \ + project_id="${GCP_PROJECT}" + +echo "Configured auth/gcp for project ${GCP_PROJECT}" + +# ── Step 3: Write Vault policies ────────────────────────────────────────────── +echo "" +echo "--- Writing Vault policies ---" + +vault policy write marketing-policy - <<'POLICY' +# marketing-policy: read access to marketing secrets +path "secret/data/marketing/*" { + capabilities = ["read"] +} +path "secret/metadata/marketing/*" { + capabilities = ["list", "read"] +} +POLICY +echo "Wrote marketing-policy" + +vault policy write soma-policy - <<'POLICY' +# soma-policy: read access to soma secrets +path "secret/data/soma/*" { + capabilities = ["read"] +} +path "secret/metadata/soma/*" { + capabilities = ["list", "read"] +} +POLICY +echo "Wrote soma-policy" + +# ── Step 4: Create GCP IAM auth roles ──────────────────────────────────────── +echo "" +echo "--- Creating GCP auth roles ---" + +# Marketing role — bound to the marketing Cloud Run SA +# type=iam means auth is validated against GCP IAM (service account JWT) +vault write auth/gcp/role/marketing \ + type="iam" \ + project_id="${GCP_PROJECT}" \ + bound_service_accounts="${MARKETING_SA}" \ + policies="marketing-policy" \ + ttl="1h" \ + max_ttl="24h" +echo "Created role: marketing (bound to ${MARKETING_SA})" + +# Soma role — bound to the soma Cloud Run SA +vault write auth/gcp/role/soma \ + type="iam" \ + project_id="${GCP_PROJECT}" \ + bound_service_accounts="${SOMA_SA}" \ + policies="soma-policy" \ + ttl="1h" \ + max_ttl="24h" +echo "Created role: soma (bound to ${SOMA_SA})" + +# ── Step 5: Smoke test ──────────────────────────────────────────────────────── +echo "" +echo "--- Smoke test ---" +echo "Verifying policies exist:" +vault policy list | grep -E "marketing-policy|soma-policy" + +echo "" +echo "Verifying auth roles exist:" +vault list auth/gcp/role + +echo "" +echo "=== Done ===" +echo "" +echo "Vault GCP auth is configured. Cloud Run services can now authenticate using:" +echo " POST \${VAULT_ADDR}/v1/auth/gcp/login" +echo " body: { role: 'marketing', jwt: '' }" +echo "" +echo "Next steps:" +echo " 1. Write secrets: vault kv put secret/marketing/... key=value" +echo " 2. Write secrets: vault kv put secret/soma/... key=value" +echo " 3. Rotate the root token: vault token create -policy=... and revoke root" +echo " 4. Store the recovery keys securely (used if KMS key needs re-wrapping)" diff --git a/servers/gcp/vault/startup.sh b/servers/gcp/vault/startup.sh new file mode 100644 index 0000000..00ad4bc --- /dev/null +++ b/servers/gcp/vault/startup.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# vault/startup.sh +# +# Boot script for Vault HA cluster nodes on GCE. +# Idempotent — safe to re-run on VM restart. +# +# Required env vars (set by the GCE metadata startup-script): +# VAULT_NODE_ID — e.g. "vault-node-1" +# VAULT_VERSION — e.g. "1.19.2" +# +# After first boot: +# 1. On vault-node-1: vault operator init +# → save root token and recovery keys in a secure location (Vault bootstrap +# requires them; the recovery keys are for KMS-sealed Vault) +# 2. Nodes 2 and 3 auto-join via retry_join once initialized +# +# Ops access: +# gcloud compute ssh vault-node-1 --zone=us-central1-a --tunnel-through-iap + +set -euxo pipefail +exec > >(tee /var/log/vault-bootstrap.log) 2>&1 + +: "${VAULT_NODE_ID:?VAULT_NODE_ID must be set}" +: "${VAULT_VERSION:?VAULT_VERSION must be set}" + +# ── Install Vault ───────────────────────────────────────────────────────────── + +apt-get update -y +apt-get install -y curl ca-certificates gnupg lsb-release unzip + +# HashiCorp GPG key + repo +curl -fsSL https://apt.releases.hashicorp.com/gpg \ + | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + +echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ + https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ + > /etc/apt/sources.list.d/hashicorp.list + +apt-get update -y +apt-get install -y vault="${VAULT_VERSION}-1" + +# Give Vault cap to lock memory (prevents secrets being swapped to disk) +setcap cap_ipc_lock=+ep /usr/bin/vault + +# ── Mount and prepare the data disk ────────────────────────────────────────── +# The data disk is attached as /dev/disk/by-id/google-vault-data +# First boot: format it. Subsequent boots: just mount it. + +DATA_DEVICE="/dev/disk/by-id/google-vault-data" +DATA_MOUNT="/opt/vault/data" +mkdir -p "${DATA_MOUNT}" + +# Only format if not already formatted (idempotent) +if ! blkid "${DATA_DEVICE}" | grep -q ext4; then + mkfs.ext4 -F "${DATA_DEVICE}" +fi + +# Mount (skip if already mounted) +if ! mountpoint -q "${DATA_MOUNT}"; then + mount "${DATA_DEVICE}" "${DATA_MOUNT}" +fi + +# Persist in fstab +DEVICE_UUID="$(blkid -s UUID -o value "${DATA_DEVICE}")" +if ! grep -q "${DEVICE_UUID}" /etc/fstab; then + echo "UUID=${DEVICE_UUID} ${DATA_MOUNT} ext4 defaults,noatime 0 2" >> /etc/fstab +fi + +# ── Derive internal IPs for retry_join ─────────────────────────────────────── +# GCE metadata server gives us the IPs of the other nodes by instance name. +# We query the Compute API via the attached SA (ADC — no key file needed). +# For the initial single-node bootstrap, retry_join failures are harmless +# (Raft simply starts as a single-node cluster until peers join). + +fetch_internal_ip() { + local instance_name="$1" + local zone="$2" + gcloud compute instances describe "${instance_name}" \ + --zone="${zone}" \ + --format="get(networkInterfaces[0].networkIP)" \ + 2>/dev/null || echo "" +} + +# Map of all Vault nodes: name → zone +declare -A NODE_ZONES +NODE_ZONES["vault-node-1"]="us-central1-a" +NODE_ZONES["vault-node-2"]="us-central1-b" +NODE_ZONES["vault-node-3"]="us-central1-c" + +# Build retry_join stanzas for all nodes other than self +RETRY_JOIN_STANZAS="" +for node in "vault-node-1" "vault-node-2" "vault-node-3"; do + if [[ "${node}" == "${VAULT_NODE_ID}" ]]; then + continue + fi + zone="${NODE_ZONES[$node]}" + ip="$(fetch_internal_ip "${node}" "${zone}")" + if [[ -n "${ip}" ]]; then + RETRY_JOIN_STANZAS="${RETRY_JOIN_STANZAS} + retry_join { + leader_api_addr = \"http://${ip}:8200\" + }" + fi +done + +# ── Get the internal IP of this node ───────────────────────────────────────── +MY_INTERNAL_IP="$(curl -sf -H "Metadata-Flavor: Google" \ + "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip")" + +# ── Write Vault config ──────────────────────────────────────────────────────── + +mkdir -p /etc/vault.d +cat > /etc/vault.d/vault.hcl </dev/null || useradd --system --home /etc/vault.d --shell /bin/false vault +chown -R vault:vault /etc/vault.d /opt/vault/data + +# ── Systemd unit ────────────────────────────────────────────────────────────── + +cat > /etc/systemd/system/vault.service <<'EOF' +[Unit] +Description=HashiCorp Vault — a tool for managing secrets +Documentation=https://developer.hashicorp.com/vault/docs +Requires=network-online.target +After=network-online.target +ConditionFileNotEmpty=/etc/vault.d/vault.hcl +StartLimitIntervalSec=60 +StartLimitBurst=3 + +[Service] +Type=notify +User=vault +Group=vault +ProtectSystem=full +ProtectHome=read-only +PrivateTmp=yes +PrivateDevices=yes +SecureBits=keep-caps +AmbientCapabilities=CAP_IPC_LOCK +CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK +NoNewPrivileges=yes +ExecStart=/usr/bin/vault server -config=/etc/vault.d/vault.hcl +ExecReload=/bin/kill --signal HUP $MAINPID +KillMode=process +KillSignal=SIGINT +Restart=on-failure +RestartSec=5 +TimeoutStopSec=30 +LimitNOFILE=65536 +LimitMEMLOCK=infinity + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable vault + +# Start or restart (restart is idempotent — re-reads config on VM metadata updates) +if systemctl is-active vault &>/dev/null; then + systemctl restart vault +else + systemctl start vault +fi + +echo "Vault ${VAULT_NODE_ID} started successfully" +echo "Run 'vault operator init' on vault-node-1 to bootstrap the cluster"