ssmm — AWS SSM Parameter Store helper for team-scoped .env sync
A small Rust CLI that treats AWS SSM Parameter Store as the source of truth
for a team's .env files, with a flat-key convention and tag-based overlays.
ssmm is intentionally narrow-scoped: it assumes you run Linux services
(typically via systemd ExecStartPre) that consume a generated .env file
with EnvironmentFile=. If that matches your setup, the tool is opinionated
enough to remove a lot of shell-script boilerplate.
Why another SSM wrapper?
ssmm materializes a .env file on disk (mode 0600) that systemd
loads via EnvironmentFile=. That makes it a drop-in replacement for
plaintext .env in existing systemd-based deployments without changing
the apps themselves. See Security model for the
disk-materialization tradeoff.
Other opinions baked in:
- Parameters live under
/<team>/<app>/<key>— the first segment is your team namespace (for IAM policy scoping), the second is the app, and the key is flat (kintone-api-token, notkintone/api/token). SecureStringvsStringis auto-detected from the key name (conservative: unknown keys default toSecureString). Only structural-looking suffixes (_path/_dir/_channel/_name/_host/_port/_region/_endpoint) map toString._urlis NOT in the safe list since URLs commonly embed credentials (e.g.postgres://user:pass@host/db, Slack webhook URLs) — URL-bearing keys staySecureStringby default. Override per key with--plain KEYif you really need plaintext.- Each parameter is automatically tagged
app=<app>so you can filter cross-namespace by tag later.
Install
Requires Rust 1.77+ (or whatever supports edition = "2024").
# or
Your IAM role needs: ssm:PutParameter, ssm:GetParametersByPath,
ssm:GetParameters, ssm:DescribeParameters, ssm:DeleteParameter(s),
ssm:AddTagsToResource, ssm:RemoveTagsFromResource,
ssm:ListTagsForResource. SSM SecureString uses the AWS-managed key
(alias/aws/ssm) by default.
Configure your team's prefix
ssmm requires an explicit prefix — set it once and forget it:
# option 1: env var (recommended for systemd services)
# option 2: per-invocation flag
If neither is set, ssmm exits with:
Error: no prefix configured. Pass --prefix /<your-team> or set $SSMM_PREFIX_ROOT=/<your-team>.
All subcommands operate under this root prefix. Parameters end up at
/<prefix>/<app>/<key>.
Quick tour
# Put a whole .env file (CWD basename becomes <app>, snake_case → dash-case)
# ↳ /myteam/your-app/kintone-api-token (SecureString [auto: default], len=...)
# ↳ /myteam/your-app/slack-channel (String [auto: suffix], len=...)
# Override the type per key when the heuristic gets it wrong
# List (CWD auto-detects app name via basename)
# Sync SSM → .env (systemd ExecStartPre friendly, mode 0600, idempotent)
# ssmm: wrote 10 variables to ./.env (app=10, shared=0, tag=0)
# Strict mode: exit non-zero if a shared / tag key is overridden by an app key
# (good for systemd ExecStartPre — you want the service to FAIL, not silently diverge)
# Show one
# Manage tags on an existing parameter
# Dashboard of every app namespace
# Find duplicates (same key across apps, or identical values)
# Migrate parameters — three safe steps (SSM has no soft-delete; go slow)
# → /tmp/ssmm-migrate-backup-<ts>.json
systemd integration
# ~/.config/systemd/user/myapp.service
[Service]
Environment=SSMM_PREFIX_ROOT=/myteam
ExecStartPre=/home/you/.cargo/bin/ssmm sync --app myapp --out /opt/myapp/.env
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/run.sh
ssmm sync is idempotent: if the generated content matches the existing
file byte-for-byte, it's a no-op (ssmm: no change).
Shared namespace and tag overlays
Values shared across multiple apps have two expressions in ssmm:
# Put a cross-app value directly under /<prefix>/shared/*
# Or tag an existing per-app parameter as shared
# sync automatically overlays /<prefix>/shared/* (disable with --no-shared)
# and any tag-matched parameter via --include-tag
Precedence when the same key name appears in multiple layers: app > include-tag > shared. Conflicts are logged to stderr.
Auto-detection
When --app is omitted, ssmm picks the name from the current directory:
/home/you/services/my_api/→my-api(snake_case → dash-case)/home/you/services/billing-svc/→billing-svc
Override with --app <name> any time.
Concurrency and throttling
SSM's PutParameter has a low per-account TPS (~3/s for standard
parameters). ssmm defaults to --write-concurrency=3 with AWS SDK
adaptive retry (max_attempts=10), so bulk imports complete without
manual backoff. Reads default to --read-concurrency=10. Both are
adjustable per invocation:
Security model
ssmm is not a hardened secret manager. It's a .env-compatible
convenience layer over SSM. Decide based on your threat model.
What ssmm sync actually does
ssmm sync calls GetParametersByPath with --with-decryption, writes
the resulting KEY=VALUE lines to the output path, and chmod 0600s
the file. Decrypted SecureString values live:
- In memory on the host running
ssmm sync - In the on-disk file (mode 0600, owner-only readable)
- In the target process's environment after
systemdreadsEnvironmentFile=(→ readable via/proc/<pid>/environto the same UID / root)
What this protects against
- Accidental commit of plaintext
.envto git (SSM is the source of truth) - Unauthorized teammates who have SSM read permission but not host login
- Drift between hosts (central management vs hand-copied
.envfiles)
What this does NOT protect against
- Host compromise: attacker with filesystem read on the host sees
plaintext
.envand the process environment - Backup exfiltration: if you back up
/opt/myapp/or/home/<user>/, plaintext secrets may end up in your backup storage - Same-host other processes under the same UID:
/proc/<pid>/environis readable by same-UID processes - Systemd journal / CloudWatch log exfiltration:
ssmmis designed so that parameter values never appear in error messages or log output (only parameter names / counts / lengths / SHA-256 hashes). If you find a path that does leak values into stderr or journalctl, please open an issue — it's considered a bug. When integrating with CloudWatch / fluent-bit / Datadog logs, verify your own wrappers also preserve this discipline
If your threat model is stricter
Consider tools that never materialize decrypted values on disk:
- chamber (Segment) — reads
SSM at
exectime and injects env vars; nothing on disk - aws-vault — similar approach for AWS credentials
- SOPS + age/KMS — encrypted-at-rest files, decrypt-on-read
ssmm's niche is specifically systemd EnvironmentFile= drop-in
replacement for plaintext .env. If you don't need that integration,
the tools above may fit your threat model better.
Similar tools
I haven't benchmarked against the following in detail, so treat this as orientation, not authoritative comparison. Issues / PRs welcome if my positioning is wrong:
- chamber — exec-time env var injection, no on-disk file
- aws-vault — AWS credential focus, related design
- dotenv-vault — hosted
.env.vaultformat
Choose ssmm when
- You already have multiple systemd services consuming
.envviaEnvironmentFile= - Migrating to
chamber execwould require touching every unit definition across teams - You accept the on-disk materialization tradeoff (mode 0600 plaintext file) in exchange for zero app-side changes
- You want a team-scoped prefix (
/<team>/<app>/<key>) and CWD-auto-detection by default
Choose something else when
- Your threat model disallows any plaintext secret on disk → use
chamber execor SOPS-with-age - You only have a handful of services and can change them →
chamber execis simpler - You're not on AWS →
dotenv-vaultor SOPS
License
MIT. See LICENSE.