Skip to content

Commit 83c3ca1

Browse files
committed
Feat: Vault Manager Service exposes gRPC API for Retrieval and Rotation of each service secret_id + app_role (No more env injection)
1 parent 70d59ef commit 83c3ca1

17 files changed

Lines changed: 1697 additions & 731 deletions

File tree

vault-manager/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
FROM golang:1.24-alpine AS build
1+
FROM golang:1.25.0-alpine AS build
22

33
WORKDIR /src
44

55
COPY go.mod go.sum ./
66
RUN go mod download
77

8-
COPY main.go ./
9-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/vault-manager ./main.go
8+
COPY . ./
9+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/vault-manager ./cmd/main.go
1010

1111
FROM alpine:latest
1212

vault-manager/Makefile

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Makefile for Go project
2+
3+
# Variables
4+
BINARY_NAME = vault-manager
5+
BINARY_DIR = bin
6+
BINARY_PATH = $(BINARY_DIR)/$(BINARY_NAME)
7+
GO = go
8+
GOFLAGS = -v
9+
10+
# Default target
11+
.PHONY: all
12+
all: build
13+
14+
.PHONY: proto
15+
proto:
16+
cd api/proto && \
17+
protoc --go_out=paths=source_relative:../../internal/vaultmanagerv1 --go-grpc_out=paths=source_relative:../../internal/vaultmanagerv1 vaultmanager.proto && \
18+
protoc --go_out=paths=source_relative:../../../pkg/vaultmanager/vaultmanagerv1 --go-grpc_out=paths=source_relative:../../../pkg/vaultmanager/vaultmanagerv1 vaultmanager.proto
19+
20+
# Ensure bin directory exists
21+
$(BINARY_DIR):
22+
mkdir -p $(BINARY_DIR)
23+
24+
# Build the binary into bin directory
25+
.PHONY: build
26+
build: $(BINARY_DIR)
27+
$(GO) build $(GOFLAGS) -o $(BINARY_PATH) cmd/main.go
28+
29+
# Run the application from bin directory
30+
.PHONY: run
31+
run: build
32+
./$(BINARY_PATH)
33+
34+
# Test the code (if you add tests later)
35+
.PHONY: test
36+
test:
37+
$(GO) test $(GOFLAGS) ./...
38+
39+
# Clean up generated files
40+
.PHONY: clean
41+
clean:
42+
$(GO) clean
43+
rm -rf $(BINARY_DIR)
44+
45+
# Format the code
46+
.PHONY: fmt
47+
fmt:
48+
$(GO) fmt ./...
49+
50+
# Vet the code for potential issues
51+
.PHONY: vet
52+
vet:
53+
$(GO) vet ./...
54+
55+
# Update dependencies
56+
.PHONY: deps
57+
deps:
58+
$(GO) mod tidy
59+
$(GO) mod download
60+
61+
# Build and run with a single command from bin directory
62+
.PHONY: dev
63+
dev: build
64+
./$(BINARY_PATH)
65+
66+
# Check for linting issues (requires golangci-lint)
67+
.PHONY: lint
68+
lint:
69+
golangci-lint run
70+
71+
# Install the binary to $GOPATH/bin
72+
.PHONY: install
73+
install:
74+
$(GO) install $(GOFLAGS)
75+
76+
# Help command to display available targets
77+
.PHONY: help
78+
help:
79+
@echo "Available targets:"
80+
@echo " all - Build the project into bin/ (default)"
81+
@echo " build - Build the binary into bin/"
82+
@echo " run - Build and run the application from bin/"
83+
@echo " test - Run tests"
84+
@echo " clean - Remove generated files and bin/ directory"
85+
@echo " fmt - Format the code"
86+
@echo " vet - Vet the code"
87+
@echo " deps - Update and download dependencies"
88+
@echo " dev - Build and run from bin/ for development"
89+
@echo " lint - Run linter (requires golangci-lint)"
90+
@echo " install - Install the binary to $$GOPATH/bin"
91+
@echo " help - Show this help message"

vault-manager/README.md

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,140 @@
11
# vault-manager
22

3-
`vault-manager` bootstraps Vault for local Persys environments.
3+
`vault-manager` bootstraps Vault for Persys Cloud environments. It brings a
4+
fresh Vault instance up to a usable state — initialized, unsealed, with a
5+
full PKI chain and per-service AppRole credentials — and then stays running
6+
as a gRPC service so other Persys Cloud components can fetch or rotate
7+
their own credentials without touching Vault Environment Variables directly.
48

59
## Responsibilities
610

7-
- Initialize and unseal Vault (first run).
8-
- Ensure PKI mounts/issuers and service roles exist.
9-
- Ensure AppRole auth, policies, and role credentials for platform services.
11+
- **Initialize and unseal Vault** on first run (single key-share setup),
12+
or pick up an already-initialized Vault via `VAULT_ROOT_TOKEN`.
13+
- **Provision the PKI chain**: mounts the root and intermediate PKI
14+
secrets engines, generates the root CA, signs and installs the
15+
intermediate CA, and configures default issuers.
16+
- **Create per-service PKI roles** so each service can request leaf
17+
certificates scoped to its own name.
18+
- **Enable AppRole auth** and create one AppRole per service, each bound
19+
to a least-privilege ACL policy (issue certs, read CA/CRL — nothing
20+
else).
21+
- **Optionally hand off from the root token** (`--secure`): provisions a
22+
scoped bootstrap-manager AppRole, logs in as it, and revokes the root
23+
token so the rest of provisioning — and the live gRPC server — never
24+
hold root privileges.
25+
- **Serve a gRPC API** (`VaultManagerService`) so services can fetch their
26+
AppRole credentials at runtime or rotate their `secret_id` without a
27+
human touching Vault.
28+
29+
## Project layout
30+
31+
```
32+
vault-manager/
33+
├── cmd/
34+
│ └── vault-manager/
35+
│ └── main.go # entrypoint: flag parsing, bootstrap orchestration
36+
└── internal/
37+
├── config/ # defaults, CLI flags, shared logger
38+
├── vaultclient/ # Vault client lifecycle: connect, init, unseal, secure handoff
39+
├── pki/ # PKI mounts, CA chain, per-service PKI roles
40+
├── policy/ # ACL policies (per-service + bootstrap manager)
41+
├── approle/ # AppRole auth, credential issuance/rotation
42+
├── server/ # gRPC API: handlers, logging interceptor, server startup
43+
└── vaultmanagerv1/ # generated protobuf/gRPC code (VaultManagerService)
44+
```
1045

1146
## Run
1247

1348
```bash
1449
cd vault-manager
15-
go run ./main.go --vault-addr=http://localhost:8200
50+
go run ./cmd/vault-manager --vault-addr=http://localhost:8200
51+
```
52+
53+
On first run against an uninitialized Vault, the unseal key and root token
54+
are printed to stdout — store them immediately, they are not recoverable
55+
afterward. On subsequent runs against an already-initialized Vault, set
56+
`VAULT_ROOT_TOKEN` in the environment instead:
57+
58+
```bash
59+
VAULT_ROOT_TOKEN=hvs.xxxxx go run ./cmd/vault-manager --vault-addr=http://localhost:8200
1660
```
1761

18-
In docker compose, this is used by `vault-manager-setup` profile in `infra/docker/docker-compose.yml`.
62+
To provision once with root and then drop root privileges for the life of
63+
the process:
64+
65+
```bash
66+
go run ./cmd/vault-manager --vault-addr=http://localhost:8200 --secure
67+
```
68+
69+
## CLI flags
70+
71+
| Flag | Default | Description |
72+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
73+
| `--vault-addr` | `https://vault:8200` | Vault API address |
74+
| `--pki-root-mount` | `pki` | Root PKI secrets engine mount path |
75+
| `--pki-int-mount` | `pki_int` | Intermediate PKI secrets engine mount path |
76+
| `--root-cn` | `Persys Cloud Root CA` | Common name for the root CA |
77+
| `--intermediate-cn` | `Persys Cloud Intermediate CA` | Common name for the intermediate CA |
78+
| `--manager-role` | `vault-manager-bootstrap` | AppRole name used for the `--secure` bootstrap handoff |
79+
| `--manager-policy` | `vault-manager-bootstrap-policy` | ACL policy name for the bootstrap manager AppRole |
80+
| `--services` | `persys-gateway,persys-scheduler,persysctl,compute-agent,persys-forgery,persys-services,persys-automation,persys-intelligence,persys-sdk` | Comma-separated list of services to provision |
81+
| `--secure` | `false` | Provision a bootstrap AppRole and revoke the root token after setup |
82+
83+
## Environment variables
84+
85+
| Variable | Required when | Description |
86+
| ------------------- | --------------------------------------------- | ---------------------------------------------------------------- |
87+
| `VAULT_ROOT_TOKEN` | Vault is already initialized | Root token used for provisioning when no fresh init occurs |
88+
89+
## What gets created in Vault
90+
91+
For each service in `--services` (default 9 platform services):
92+
93+
- A PKI role at `<pki-root-mount>/roles/<service>` (EC P-256 keys, 72h
94+
default TTL, 720h max TTL, any name allowed for internal cert issuance).
95+
- An ACL policy named `<service>-policy`, granting:
96+
- `update` on `<pki-root-mount>/issue/<service>`
97+
- `read` on `<pki-root-mount>/cert/ca`, `cert/ca_chain`, and `crl`
98+
- An AppRole at `auth/approle/role/<service>`, bound to that policy
99+
(1h token TTL, 4h max TTL, 24h secret_id TTL, unlimited secret_id uses).
100+
101+
If `--secure` is set, an additional bootstrap-manager AppRole and a
102+
broader policy (mount/auth/policy management plus full PKI access) are
103+
created, used once to hand off from the root token, then the root token
104+
is revoked.
105+
106+
## gRPC API
107+
108+
`vault-manager` listens on `:50069` and exposes `VaultManagerService`
109+
(defined in `internal/vaultmanagerv1`):
110+
111+
- **`GetServiceCredentials(service_name)`** — returns the service's
112+
current `role_id` and a freshly generated `secret_id`, plus an
113+
`expires_at` Unix timestamp (720h from issuance).
114+
- **`RotateServiceSecretID(service_name)`** — identical behavior to
115+
`GetServiceCredentials`; every call mints a new `secret_id`, so calling
116+
either RPC rotates the credential. They're exposed as two RPCs for
117+
clarity of intent at the call site (initial fetch vs. explicit
118+
rotation), not because the underlying operation differs.
119+
120+
Every RPC is wrapped in a logging interceptor that emits structured JSON
121+
logs (method, status code, duration, and any error) for each call.
122+
123+
## Docker Compose
124+
125+
In docker compose, this is used by the `vault-manager` profile in
126+
`infra/docker/docker-compose.yml`.
127+
128+
## Operational notes
129+
130+
- Single key-share initialization (`secret_shares: 1`, `secret_threshold:
131+
1`) is intended for local/dev environments. Production Vault deployments
132+
should use Shamir's Secret Sharing with multiple key holders or
133+
auto-unseal via a cloud KMS instead.
134+
- The printed root token and unseal key during first-run initialization
135+
are the only time they're surfaced — capture and store them securely
136+
immediately (e.g., in your team's secrets manager), since Vault does not
137+
let you retrieve them again.
138+
- `secret_id_num_uses: 0` (unlimited) on service AppRoles means a leaked
139+
`secret_id` can be reused until its 24h TTL expires; rotate via the
140+
gRPC API or by re-running `vault-manager` if a leak is suspected.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
syntax = "proto3";
2+
3+
package vaultmanager;
4+
5+
option go_package = "github.com/persys-dev/persys-cloud/pkg/vaultmanager";
6+
7+
service VaultManagerService {
8+
rpc GetServiceCredentials(GetServiceCredentialsRequest) returns (ServiceCredentialsResponse);
9+
rpc RotateServiceSecretID(RotateServiceSecretIDRequest) returns (ServiceCredentialsResponse);
10+
}
11+
12+
message GetServiceCredentialsRequest {
13+
string service_name = 1;
14+
}
15+
16+
message RotateServiceSecretIDRequest {
17+
string service_name = 1;
18+
}
19+
20+
message ServiceCredentialsResponse {
21+
string role_id = 1;
22+
string secret_id = 2;
23+
int64 expires_at = 3; // Unix timestamp seconds
24+
string message = 4;
25+
}

0 commit comments

Comments
 (0)