Storing and managing secrets cheaply and safely
Every application has its secrets (database passwords, API keys, tokens, etc.) and they need to live somewhere that isn't a plaintext file committed to Git (please, don't). For those familiar with AWS, the first thought that comes to mind is to go with Secrets Manager to manage them, and it works great, except for the price per secret per month. Later down the road, it quietly turns into a line item nobody remembers approving once you multiply it across services, environments and regions. The good news is: for most secrets, there's a cheaper place to put them that's just as safe.
Storing secrets with SSM Parameter Store
Parameter Store
is part of AWS Systems Manager and it holds configuration values for your
applications. For anything sensitive, you use the
SecureString type, which encrypts the value at rest using
AWS KMS; for everything else you may either use String or
StringList. The best part is that the
standard tier is free
and you can get a decent amount of storage and other benefits without paying a
cent.
Writing a secret is a one-liner and if you don't point it at a specific key,
it uses the AWS managed key aws/ssm that Systems Manager
creates for you, which costs nothing to have:
aws ssm put-parameter \
--name "/myapp/prod/my-secrets" \
--value "super-secret-value" \
--type SecureString
Reading it back requires --with-decryption, otherwise
you'll just get the ciphertext staring back at you:
aws ssm get-parameter \
--name "/myapp/prod/my-secrets" \
--with-decryption \
--query Parameter.Value \
--output text
A nice bonus is that the path-like naming isn't just for looks. You can scope
IAM permissions to a prefix like /myapp/prod/*, so a
service only ever sees the secrets it's supposed to and nothing from other
paths.
Comparing with AWS Secrets Manager
Both services encrypt with KMS, so from a security standpoint they're basically the same. What you're actually paying for with Secrets Manager is a set of features Parameter Store doesn't have:
- Lambda rotations included in the price;
- Native integration with RDS, Redshift and DocumentDB, so rotating database credentials is mostly wired up already (managed rotation);
- Cross-region replication and easier cross-account sharing.
Breaking it down, if you have credentials that auto-rotate through Secrets Manager, leave them there. Rebuilding rotation yourself to save a few cents might not be a good use of your time. However, a large chunk of secrets might never rotate at all - static API keys, third-party tokens, webhook signing secrets and so on. Those are often wasting money with Secrets Manager.
For those, Parameter Store solves the problem for free. And if you ever bump into the size limit or need parameter policies, advanced parameters are still way cheaper than Secrets Manager, in case someone tries to guilt you about it. However, just like every other AWS' product, Secrets Manager and Parameter Store both have quotas and limits, so check and compare before committing.
Watch out for the KMS decryption bill
Now, there's always a caveat (another one). Parameter Store doesn't charge you
to store a SecureString, but the decryption and other
requests, aren't free.
Despite having a free tier, every single
get-parameter --with-decryption triggers a KMS
Decrypt call, and if you take a look at the pricing, it's
nothing, right?!
Now, picture a Lambda that pulls fifteen separate secrets on every request, receiving traffic all day, plus a couple of CI pipelines fetching the same values on every build. Suddenly you're making KMS requests like you're trying to break a record. Also, if those parameters are on the advanced tier, you're also paying just to store them separately.
So, if applicable, group related secrets into a single
SecureString parameter as a JSON string. One parameter,
one decrypt call per fetch, one thing to manage.
aws ssm put-parameter \
--name "/myapp/prod/my-secrets" \
--type SecureString \
--value '{"db_password":"...","api_key":"...","jwt_secret":"..."}'
Then you fetch it once and let the application pull out whatever it needs:
RAW=$(aws ssm get-parameter \
--name "/myapp/prod/my-secrets" \
--with-decryption \
--query Parameter.Value \
--output text)
echo "$RAW" | jq -r '.db_password'
Also, since I don't want you creating new problems: both Parameter Store tiers have a storage cap, so don't try to cram every secret you own into one blob - group them by what's actually fetched together, usually per service or per environment. Adding to that, since the whole value is one parameter, changing a single field means rewriting the entire JSON, so anything that needs independent rotation might be better off on its own. It's a trade-off, not a silver bullet.
Wrap-up
Reach for SSM Parameter Store as your cheap-and-safe default and group the values into a JSON (beware of the storage limit) so KMS doesn't nickel-and-dime you on every request. Keep Secrets Manager for the handful of secrets where rotation and native integrations actually earn their price. Same security, a fraction of the bill.