HashiCorp Vault vs AWS Secrets Manager in Python
Both store secrets and rotate them; they differ in how credentials are issued and how tightly they bind to a cloud. For a Python team the decision usually comes down to “are we all-in on AWS?” and “do we need dynamic, short-lived credentials?” This page compares them concretely, building on the enterprise secrets management overview.
Those two questions are worth separating, because they pull in different directions and teams often answer only the first. Cloud posture is a strategic fact about the organisation; the credential model is a technical property of each individual secret. A team entirely on AWS may still want dynamic database credentials, and a multi-cloud team may have secrets that cannot be minted on demand regardless of which store holds them.
Problem 1: assuming they are interchangeable
# ANTI-PATTERN: picking one without considering the auth and credential model
secret = get_secret("db") # static? dynamic? IAM-scoped? AppRole? it matters
The retrieval call looks similar, but the credential model behind it is the real difference. With Secrets Manager, get_secret("db") returns the same password every caller receives until someone rotates it; with Vault’s database engine, the equivalent call creates a new database user that exists for the length of its lease and is then deleted.
That difference propagates into the surrounding code. A static value can be read once at startup and held; a dynamic one has a lifetime, so the application needs a refresh policy, a way to rebuild connections, and a retry for the request that arrives as a lease expires. Choosing the store without noticing that is how a team adopts dynamic credentials and then writes client code that treats them as passwords.
The authentication model differs just as much. boto3 picks up an IAM role from the environment with no code at all — the identity is ambient, supplied by the platform. Vault’s AppRole requires the application to log in explicitly with a role_id and a runtime-delivered secret_id, which is more code and more moving parts, and is also precisely what makes the same code work identically on a laptop, in a data centre, and in a cloud that is not AWS.
Problem 2: static credentials where dynamic ones fit
A long-lived password in either store is weaker than a Vault dynamic credential that expires in an hour. If your backends support dynamic secrets, a static-only approach leaves value on the table.
The gap is larger than it sounds. A static credential remains valid from the moment it leaks until somebody notices and rotates it — a window measured in weeks at best. A dynamic credential is invalid within its TTL whether or not anyone noticed, which removes detection from the critical path entirely.
The second benefit is attribution. Every dynamic credential is issued to a named role at a recorded time and produces a distinct database username, so “which service ran this query at 03:00?” is a lookup. A shared static credential makes every consumer indistinguishable, and that is exactly the question an incident investigation opens with.
Neither of those requires abandoning a managed store. AWS Secrets Manager holds the vendor API keys and the service configuration; a dynamic engine issues the database credentials. Running both is normal, and the decision is per credential rather than per organisation. The provider interface below is what makes that practical: two small functions, one return type, and an application that never learns which store supplied which value.
Comparison
| Dimension | HashiCorp Vault | AWS Secrets Manager |
|---|---|---|
| Credential model | Dynamic, short-lived (and static KV) | Static, with scheduled rotation |
| Auth in Python | AppRole via hvac |
IAM role via boto3 |
| Cloud binding | Cloud-agnostic | AWS-native |
| Rotation | Lease TTL / rotation engines | Built-in (esp. RDS) |
| Ops burden | You run/secure Vault | Fully managed |
| Best when | Multi-cloud, dynamic creds | All-in on AWS |
The ops-burden row is the one that decides most real adoptions, and it is easy to underweight when comparing feature lists. Running Vault in production means a highly available cluster, a storage backend, an unseal strategy, upgrades, backups, and a disaster-recovery plan for a system that every service depends on to start. HashiCorp’s managed offering removes much of that, at which point the comparison becomes a cost and data-residency question rather than an operational one.
The rotation row hides a similar asymmetry. Secrets Manager’s RDS rotation is genuinely turnkey — enable it, and a managed Lambda handles the four-step rotation. Vault’s database engine is more capable and expects you to configure the role, the creation statements, and the grants yourself. More flexible, more to get right — and the flexibility is genuinely valuable when the database is not one of the handful the managed rotation supports.
The cloud-binding row deserves a nuance too. “Cloud-agnostic” describes Vault’s API, not its deployment: a Vault cluster still runs somewhere, and running it in one cloud to serve workloads in three means a cross-cloud network dependency on the path of every service’s startup. That is workable and it is not free, and it is worth planning before the second cloud arrives rather than after.
Where they converge is the application code. Both are fetched behind a small provider function, both should be cached with a TTL, and both should return SecretStr. The differences that matter live in operations and in credential lifetime, not in the shape of the call.
Secure implementation
# secrets/provider.py — one interface, swap the backend
from pydantic import SecretStr
def from_vault(role: str) -> SecretStr:
import hvac
c = hvac.Client(url="https://vault.internal:8200")
c.auth.approle.login(role_id=RID, secret_id=SID.get_secret_value())
data = c.secrets.database.generate_credentials(name=role)["data"] # dynamic
return SecretStr(data["password"])
def from_asm(secret_id: str) -> SecretStr:
import boto3
raw = boto3.client("secretsmanager").get_secret_value(SecretId=secret_id)
return SecretStr(raw["SecretString"]) # static + rotated
Wrap whichever backend you choose behind one interface returning SecretStr, so the application does not care which store it is. See Vault and AWS Secrets Manager for each in depth.
The interface is worth being honest about, though: it abstracts retrieval and cannot abstract lifetime. A caller that receives a SecretStr from from_vault holds something that expires; the same call to from_asm returns something that does not. Code written against the interface must therefore assume the stricter contract — re-fetch periodically, handle an authentication failure by refreshing, never cache indefinitely — because that behaviour is correct for both and only one of the two tolerates the alternative.
Designing for the stricter contract from the start also makes migration cheap. A service that already re-fetches and rebuilds on change can move from Secrets Manager to a dynamic engine by changing one function, whereas one that reads once at startup needs its whole consumer layer revisited.
The local imports in each function are a small deliberate touch: a service using only one backend never imports the other’s SDK, so hvac stays out of an AWS-only deployment and boto3 out of a Vault-only one. That keeps the image smaller and, more usefully, keeps a dependency you are not exercising out of the security surface you have to track for updates.
Migrating between them without a flag day
Because the provider interface hides retrieval, moving a credential from one store to the other is a per-secret operation rather than a platform migration. Doing it one credential at a time keeps every step reversible.
# secrets/provider.py — read from the new store, fall back to the old one
import logging
from pydantic import SecretStr
log = logging.getLogger(__name__)
def get_db_password(vault_role: str, asm_id: str) -> SecretStr:
try:
value = from_vault(vault_role) # the new source
log.info("secret source=vault key=db_password")
return value
except Exception:
log.warning("secret source=asm key=db_password reason=vault_unavailable")
return from_asm(asm_id) # the old source, still populated
The migration then runs in four steps that each stand on their own. Populate the new store alongside the old and leave both current. Deploy the dual-read above and watch the source log: every line saying source=vault is a service that has moved, and every source=asm line is one that has not. Once the fallback stops firing across a full traffic cycle, remove it. Only then decommission the old secret.
The source log is what makes this safe rather than hopeful. Without it, “have we finished migrating?” is answered by reading code and hoping nothing was missed; with it, the answer is a query over log lines, and a straggling consumer announces itself.
Ordering credentials by risk makes the whole exercise cheaper. Start with something low-stakes — a staging database, a read-only reporting role — so the first exposure to a lease-expiry bug happens where it costs nothing. By the time you reach the production database, the refresh, rebuild, and retry machinery has been exercised in production for weeks against something nobody would page about.
The one thing not to do is migrate both the store and the credential model in a single step. Moving a static secret from Secrets Manager to Vault’s KV is a storage change; switching from a static password to dynamic credentials is a lifetime change that touches the consumer layer. Doing them separately means a failure has one obvious cause.
Gotchas & version-specific behaviour
- Vault dynamic credentials expire with their lease — design for re-fetch; ASM values persist until rotated.
- ASM’s RDS rotation is turnkey; Vault’s database engine is more flexible but you operate it.
- Vault is cloud-agnostic; ASM ties you to AWS IAM and KMS.
- Both should be wrapped in
SecretStrand cached with a short TTL. - Secrets Manager charges per secret per month plus per API call, so a per-request fetch is a cost problem as well as a rate-limit one.
- Vault’s availability becomes your availability — cache for the lease duration so a restart does not take your services with it.
Those last two are the operational realities that surface after adoption rather than during evaluation. The Secrets Manager cost model rewards the caching this section recommends anyway, so it rarely bites a service that was built correctly. The Vault availability point is more consequential: a Vault outage while every service holds a valid cached credential is invisible, while the same outage with per-request fetching is a total outage — which makes the caching decision an availability decision rather than a performance one.
Production parity checklist
- The choice matches your cloud posture (AWS-only vs multi-cloud).
- Dynamic credentials are used where the backend supports them.
- Secrets are wrapped in
SecretStrbehind one provider interface. - Rotation is automated in whichever store you pick.
- Access is least-privilege (IAM resource scoping or Vault policies).
- Client code assumes credentials can expire, even where the current store’s do not.
Auditing an existing setup against this list usually turns up one specific gap: a service that reads its secret once at startup because the current store’s values never expire. That is not wrong today and it is what makes a later move to dynamic credentials expensive, so it is worth fixing while it is a small change rather than after the decision to migrate has been made.
Key takeaways
Choose AWS Secrets Manager when you are all-in on AWS and want managed rotation; choose Vault when you need cloud-agnostic, dynamic, short-lived credentials. The honest version of that advice is that most teams need both — a managed store for the vendor keys and configuration that cannot be minted on demand, and a dynamic engine for the database and cloud credentials that can.
What matters more than the choice is writing client code to the stricter contract: fetch through one small provider, cache with a TTL, assume the value can expire, and rebuild consumers when it changes. Do that and the decision stops being irreversible — moving a credential from one store to the other becomes a one-function change rather than a project.
That reversibility is worth more than getting the initial choice exactly right. Teams change clouds, acquire services with different stacks, and discover that one credential needs a model the chosen store does not offer — and a codebase that can absorb those changes by swapping a provider function will handle all of them cheaply. A codebase that reads a specific store’s SDK directly in fifteen modules will not, whichever store it happened to pick. For a Kubernetes-specific comparison, see Doppler vs Vault for Kubernetes.