In this episode we'll round out our understanding of secrets engines with three engines commonly used in the field: TOTP for centralized 2FA, SSH for OTP login and CA signing, and AWS for short-lived dynamic IAM credentials. We'll see when to use which engine.

After covering the PKI Secrets Engine for TLS certificate automation in episode 7, this episode completes Phase 2 of this series with three additional secrets engines that are no less common in the real world: TOTP for centralized two-factor authentication, SSH for securing server access without permanent passwords, and AWS for creating short-lived dynamic IAM credentials.
Why does this topic matter in the real world? Because these three engines touch different everyday problems: how teams secure accounts that use 2FA (TOTP), how admins manage thousands of SSH accesses to servers without having to memorize or store uncontrollable private keys (SSH), and how pipelines or applications access the cloud without storing long-lived AWS access keys (AWS). All three use the same principle we've built throughout this series: secrets are not stored permanently, but managed and short-lived.
Let's break them down one by one, complete with hands-on practice.
TOTP (Time-based One-Time Password) is the most popular 2FA mechanism — applications like Google Authenticator use it. Usually, everyone enrolls their account into the TOTP app on their own phone. But in a company, distributing TOTP secrets to employees' personal phones can be both a security and operational problem.
The Vault TOTP secrets engine allows Vault to become the centralized OTP generator and validator. The flow:
vault read totp/code/my-account) or validated in Vault (vault write totp/code/my-account code=...).vault secrets enable totpOutput:
Success! Enabled the totp secrets engine at: totp/Create a key for an account:
vault write totp/keys/my-account \
url="otpauth://totp/MyOrg:arman@devvnull.local?secret=MY2PRIVATE&issuer=MyOrg"Output:
Key Value
--- -----
url otpauth://totp/MyOrg:arman@devvnull.local?secret=MY2PRIVATE&issuer=MyOrgNote
The secret in the url above can be a random value you generate yourself, or you can let Vault create it by using the key and generate=true arguments. The important thing: the otpauth://totp/... format is the standard understood by all authenticator apps (Google Authenticator, Authy, Aegis, etc.).
Generate the current OTP code (for example, to fill in a login form from the server side):
vault read totp/code/my-accountOutput:
Key Value
--- -----
code 482913Validate a code entered by the user:
vault write totp/code/my-account code=482913Output:
Key Value
--- -----
valid trueTip
In production, the TOTP engine is usually combined with another authentication method (for example, a login app using userpass + TOTP validation in Vault). Its core value: OTP codes don't need to be shared with employees' personal phones, validation is centralized, and the audit trail is complete — who validated which code when.
The classic SSH access problem: admins store private keys on their own laptops, or — worse — permanent SSH passwords shared by everyone. When one laptop is lost, the entire infrastructure is at risk. The Vault SSH secrets engine offers two different modes:
| Aspect | SSH OTP | SSH CA |
|---|---|---|
| Mechanism | Vault creates a one-time password for login | Vault signs the user's (and host's) public key |
| Access method | ssh arman@server then enter the OTP | The SSH client uses a private key signed by Vault |
| Duration | One login (single-use OTP) | Per the key signing TTL |
| Server-side install requirements | Vault SSH helper + OTP verifier on the server | Only trust the user CA on the server (/etc/ssh/trusted-user-ca-keys) |
| User key rotation | Not relevant (new code every login) | Key re-signed per TTL |
| Best for | Small environments / human users | Large scale, automation, and service accounts |
vault secrets enable sshvault write ssh/roles/otp-role \
key_type=otp \
default_user=ubuntu \
cidr_list=10.0.0.0/8When a user wants to log in, Vault provides an OTP code:
vault read ssh/creds/otp-roleOutput:
Key Value
--- -----
key a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
key_type otp
port 22
username ubuntuWith this OTP key, the user can log in to the server: ssh ubuntu@10.0.0.5 then enter the OTP key as the password. After being used once, that OTP is no longer valid.
CA mode is more elegant for large scale. The principle: Vault holds an SSH CA, and servers are configured to trust keys signed by Vault. Users don't need to store secrets in Vault — just their signed public key.
vault write ssh/roles/ca-role \
key_type=ca \
allowed_user_domains=dev,ops \
default_extensions="permit-pty,permit-port-forwarding" \
allowed_users=ubuntuUsers submit their public key to be signed:
vault write ssh/sign/ca-role \
public_key=@/home/arman/.ssh/id_ed25519.pub \
valid_principals="arman"Output:
Key Value
--- -----
key_type ca
signed_key ssh-ed25519-cert-v01@openssh.com AAAA... (SSH certificate, not just a public key)The resulting signed_key is an SSH certificate (not just a public key) with an expiration date. Servers that trust the Vault CA will automatically accept this key until its TTL expires — after that, the user must request a new signature.
Vault can also sign host keys (the server's own identity), so clients can verify that the server they're connecting to is really the company's server — preventing man-in-the-middle attacks:
vault write ssh/sign/host-signer \
public_key=@/etc/ssh/ssh_host_ed25519_key.pub \
valid_principals="dev-server-01"Output:
Key Value
--- -----
key_type ca
signed_key ssh-ed25519-cert-v01@openssh.com AAAA... (server host certificate)The combination of user CA + host CA is what forms zero-trust SSH: users verified via certificates, servers verified via certificates, and both short-lived. No more permanent passwords or host keys left unchecked.
Warning
A crucial difference: with SSH OTP, the server needs a Vault SSH helper plugin to verify the one-time password. With SSH CA, all that's needed is adding the Vault CA public key to the server's trusted-user-ca-keys — far lighter and no extra processes on the server. For large infrastructure, SSH CA is almost always the better choice.
The classic cloud problem: AWS access keys created once and living for years, stored on laptops, in environment variables, or — not infrequently — in Git commits. If leaked, attackers get full access to the AWS account.
The AWS secrets engine uses the same dynamic secrets principle as the database engine in episode 5: Vault creates temporary IAM credentials that automatically expire.
vault secrets enable awsOutput:
Success! Enabled the aws secrets engine at: aws/Configure the AWS credentials Vault uses to manage IAM (create users / sign STS tokens):
vault write aws/config/root \
access_key="AKIAIOSFODNN7EXAMPLE" \
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
region="ap-southeast-1"Create a role defining the IAM policy for dynamic credentials:
vault write aws/roles/my-role \
credential_type=iam_user \
policy_document=-<<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": "*"
}
]
}
EOFNow, whenever a pipeline or application needs AWS access:
vault read aws/creds/my-roleOutput:
Key Value
--- -----
lease_id aws/creds/my-role/1h2VzQ6xW9pLcN4mB7kR5sT0dF8jK3y
lease_duration 768h
lease_renewable true
access_key AKIAVPQEXAMPLEDYNAMICKEY
secret_key 5hS9mZcQwE3rTgY2uNvB6lK7xDfC0jJp
security_token <nil>Important
For credential_type=iam_user, Vault creates a new IAM user in the AWS account along with its access key, and deletes that user when the lease ends. For credential_type=federation_token or assumed_role, Vault uses AWS STS so no new user is needed — access comes via a session token with a shorter TTL (up to 1 hour for federation). Choose accordingly: iam_user for the 768-hour need (the default maximum), STS-based for short-lived credentials.
Example of another commonly used command:
vault read aws/creds/my-sts-roleOutput:
Key Value
--- -----
access_key ASIAVPQEXAMPLESHORTLIVED
secret_key pQn1sLvXc3rTgY5uNhB7jK9mZdF2wA6y
security_token IQoJb3JpZ2luX2Vj... (long STS session token)
lease_id aws/creds/my-sts-role/6sR4tG8vY2mC5xH7bN9kQ3wL0zF1jD8
lease_duration 1hTip
This is why the CI/CD pipeline in episode 18 later can run without storing a single AWS access key in the repository: when the pipeline needs access, it just requests dynamic credentials from Vault, uses them while the job runs, and they're gone once the lease ends. No more AWS secrets nesting in GitHub/GitLab for years.
To make the picture of the three engines more concrete, let's weave them into a single scenario: a company managing an internal data platform.
| Layer | Need | Engine Used |
|---|---|---|
| Human access | Engineers log in to the admin dashboard | TOTP (2FA) |
| Server access | Engineers SSH into production servers | SSH CA (user key signing) |
| Automation | Pipelines read data from S3 | AWS (dynamic IAM credentials) |
One and the same Vault cluster serves all three — each with its own mount, role, and policy. This is the beauty of Vault: one control plane for every kind of secret, from OTP codes to cloud credentials, with a single audit model and a single access model.
These three engines serve different needs — don't mix them up:
| Need | Engine | Analogy |
|---|---|---|
| 2FA / one-time codes for app login | TOTP | A single-use card for entering the building |
| SSH access to servers | SSH (OTP / CA) | A parking barcode scanned once, or a verified access card |
| AWS cloud access | AWS | Contract employees whose term automatically ends |
Note
A rule of thumb: choose the engine based on what you want to control. Want to control the login code → TOTP. Want to control who can SSH where → SSH. Want to control who can access the cloud with what policy → AWS. All three actually use the same machinery underneath: secrets engine + lease + policy + audit.
| Engine | Mistake | Symptom | Solution |
|---|---|---|---|
| TOTP | otpauth URL in the wrong format | Code doesn't appear in the authenticator | Use the otpauth://totp/{issuer}:{account}?secret={base32} format |
| TOTP | Validating an already-expired code | valid=false | TOTP codes are only valid for ~30 seconds; validate immediately after receipt |
| SSH OTP | Forgot to install the helper on the server | OTP login rejected | Make sure vault-ssh-helper is active and the OTP role uses the right cidr_list |
| SSH CA | Forgot to add the trusted CA on the server | Host key verification failed / key rejected | Add the Vault CA public key to the server's trusted-user-ca-keys |
| AWS | Using iam_user when a short TTL is needed | Many IAM users pile up | Use federation_token / assumed_role via STS |
| AWS | Policy document encoded incorrectly | Invalid policy JSON error | Test with a matching credential_type and valid JSON policy |
| All | Tokens/leases used but never renewed | Connections drop suddenly | Apply renewal / re-fetch before the TTL ends (episode 12) |
In episode 8, we've rounded out our understanding of secrets engines with three additional ones: TOTP for centralized 2FA OTP generation and validation, SSH with two modes (single-use OTP vs CA signing) for securing server access, and AWS for short-lived dynamic IAM credentials. We also saw the comparison of when to use which engine, and the common traps in each.
The essence of this episode: all Vault secrets engines follow the same pattern — configure once, then credentials are managed and short-lived, with centralized, policy-controlled, audited access. Whatever technology you're securing — databases, data, certificates, logins, or the cloud — the mindset is consistent.
With Phase 2 complete, you've mastered all of Vault's core secrets engines. In episode 9, we'll enter Phase 3: Vault Policies — how to control who can access what, using the HCL language and the capabilities concept that determines the security of the entire system. Keep your enthusiasm up!