So I've been deep in the Vault weeds lately, specifically with the SSH OTP engine. It's fantastic for granting short-lived, one-time SSH access, but the user experience for developers always felt a bit... clunky. Needing to run CLI commands or remember specific API endpoints was a friction point.
I built a simple internal web portal to abstract that away. The goal was: a dev goes to a page, selects a target host (from a pre-approved list), and gets a single-use OTP they can paste directly into their SSH prompt. No `vault read` commands needed.
The core is a tiny Go service that acts as a privileged intermediary. It uses Vault's Go client with an approle that has strictly limited permissions (just `write` to `ssh/creds/`). The frontend is just some HTML/JS that talks to this backend API.
Here's the heart of the backend logic that creates the OTP:
```go
func generateOTP(host string) (string, error) {
// config & client setup omitted for brevity
secret, err := client.Logical().Write(
fmt.Sprintf("ssh/creds/otp_key_role"),
map[string]interface{}{
"username": "ec2-user", // or from host mapping
"ip": host,
},
)
if err != nil {
return "", err
}
otp, ok := secret.Data["key"].(string)
if !ok {
return "", fmt.Errorf("no OTP in response")
}
return otp, nil
}
```
The frontend then displays it in a `` block with a copy button. We also added a simple audit log by having the backend log the request (host, requester ID, timestamp) – which is less for security (Vault already audits the actual credential creation) and more for us to see what hosts are popular.
Some interesting observations:
* The overhead is negligible since the portal is just proxying one authenticated write call.
* We cache the list of allowed hosts from a separate config file, not Vault, to keep responsibilities separate.
* The biggest win was actually in documentation 😅. Instead of a multi-step guide, it's now "go to this URL, pick the host, copy the code."
Has anyone else built similar internal tooling around Vault's secrets engines? I'm curious about alternative patterns, especially around self-service workflows for other credential types (like database logins). Also, any clever ways you've tied this into broader infra access request systems?
System calls per second matter.
That's a clean approach to abstracting the Vault API. I've implemented something similar for service account key generation, also with a Go backend using approle.
One operational detail you might consider: logging and audit. While Vault's audit log will show the privileged write from your service's approle, you lose the mapping of which end-user requested which OTP unless you capture that in your application logs. We found it necessary to log the requesting user principal (from your frontend auth) alongside the target host and a request ID, then correlate that with Vault's lease ID. It adds a few fields to your struct but makes forensic tracing possible.
Also, how are you handling the pre-approved host list? We started with a static config but moved it to a periodic sync from our CMDB, which reduced drift when instances were decommissioned.
data is the product