Encrypting and Decrypting Secrets for Environment Definitions via CLI
The Baseline Problem: Secrets and Git Don't Mix
Every environment definition eventually needs a real credential — a database password, a third-party API key, a signing secret. The naive approach (write it directly as a plaintext string in bunnyshell.yaml) works exactly until that file is committed to git, at which point the plaintext exists permanently in history, recoverable by anyone with repository access regardless of whether the file is edited afterward.
Bunnyshell's CLI gives you two ways to avoid this entirely, and understanding when to use each is the actual point of this guide.
Option A: SECRET["..."] — Encrypt on Save
For interactive authoring, you can write the plaintext value directly in the YAML, wrapped in SECRET[...]:
1environmentVariablesGroups:
2 app:
3 DB_PASSWORD: SECRET["actual-plaintext-password"]Bunnyshell encrypts this server-side the moment the environment is created or updated from this file — the plaintext only exists in transit for that one operation. This is confirmed directly: after creating an environment with a SECRET["..."] value, exporting the environment's live definition back shows the encrypted form, not the original plaintext:
1$ bns environments definition --id LPDqgKKEzk
2
3environmentVariablesGroups:
4 app:
5 DEMO_SECRET: 'ENCRYPTED[Q2dncFYIpW629swUrDuO0RJLADm2thdcUDcwttbm0mORfluCtcEgQ6t9PqkWKlFgWucJu5XvQJGnhMr3AMPilZEybndubHQ=]'This is genuinely useful for a first pass or for values you're setting interactively and don't intend to commit in the SECRET[...] form at all — but note the caveat below about what "the plaintext only exists in transit" actually means for your local file and shell history.
Option B: ENCRYPTED[...] — Pre-Encrypt, Never Write Plaintext to a File
For anything that will live in a git-tracked file reviewed by other people — which is the common case for a team-shared bunnyshell.yaml — the safer pattern is encrypting the value before it ever touches a file, using the CLI directly:
1$ echo -n "hunter2-db-password" | bns secrets encrypt --organization NA1Q2kyalg
2
3Expression | ENCRYPTED[ckXzb+vOS9ebl6M+uYcYJnjrCWg6ZSUEjxgELT6/SRI3d18a4dGCBLXIypJ+UjnilZEybndubHQ=]Note the -n on echo — without it, echo appends a trailing newline to the value, which gets encrypted as part of the string, and the resulting secret silently becomes "hunter2-db-password\n" instead of the intended value. This is a genuinely easy mistake to make and a genuinely confusing one to debug later (an auth failure with no obvious cause, because the password is almost right), so it's worth internalizing as a rule: always pipe secrets to bns secrets encrypt via echo -n, never bare echo.
You can also pass the value as a positional argument instead of piping it, though this leaves the plaintext briefly visible in your shell history and process list, which is a real (if often academic) exposure on a shared machine:
1$ bns secrets encrypt "another-plaintext-secret" --organization NA1Q2kyalg
2
3Expression | ENCRYPTED[qO8fTvKr/E5+4nHMa/0OOz6/fXed0554ihdj+FLe2KWO5jvAHWAQtN3C5GqqTpHmL3efNeKVkTJud25sdA==]The resulting ENCRYPTED[...] string is what you write into bunnyshell.yaml directly:
1environmentVariablesGroups:
2 app:
3 DB_PASSWORD: 'ENCRYPTED[ckXzb+vOS9ebl6M+uYcYJnjrCWg6ZSUEjxgELT6/SRI3d18a4dGCBLXIypJ+UjnilZEybndubHQ=]'This file can be committed and reviewed safely — the ciphertext is meaningless without appropriate access to your Bunnyshell organization to decrypt it, and the plaintext never existed in any file at all.
Decrypting: Verifying What's Actually Configured
When debugging a credential-related failure, it's useful to confirm what value is actually stored rather than assuming based on when it was last set (which is a surprisingly common source of wasted debugging time — "I definitely set that to the right value last week" is not the same as verifying it now):
1$ bns secrets decrypt "ENCRYPTED[qO8fTvKr/E5+4nHMa/0OOz6/fXed0554ihdj+FLe2KWO5jvAHWAQtN3C5GqqTpHmL3efNeKVkTJud25sdA==]" --organization NA1Q2kyalg
2
3Value | another-plaintext-secretThis round-trips correctly — the decrypted value matches exactly what was originally encrypted, confirmed directly against the live organization above. Decryption requires the same organization-level access as encryption; someone with only the ciphertext string (found in a shared bunnyshell.yaml, for instance) and no actual Bunnyshell organization access cannot recover the plaintext from it.
Encrypting an Entire Definition File at Once
For migrating an existing bunnyshell.yaml that has plaintext values scattered through it into fully encrypted form, rather than hand-encrypting each value individually, the CLI supports encrypting an entire file's SECRET[...] values in one pass — check bns environments definition --help and your CLI version's exact flag for this bulk-encrypt operation, since the exact invocation has shifted between CLI versions (this guide was written against bns 0.26.1).
The One Mistake That Corrupts a Secret Silently: Double-Encryption
# WRONG — do not do this
DB_PASSWORD: SECRET["ENCRYPTED[Q2dncFYIpW629swUrDuO0RJ...]"]Wrapping an already-encrypted ENCRYPTED[...] value inside SECRET[...] double-encrypts it. The resulting value is not simply wrong — it's a string that will fail to decrypt correctly at runtime, and depending on how your application handles a decryption failure, this can manifest as anything from a clear error to a subtly wrong credential being passed through, which is a much harder failure to diagnose. SECRET[...] and ENCRYPTED[...] are two separate entry points into the same encrypted-storage system — pick one per value, never nest them.
If you're unsure whether a value already went through bns secrets encrypt, check for the ENCRYPTED[ prefix. If it's already there, write it directly — do not wrap it in SECRET[...] "just to be safe."
A Practical Workflow for a New Secret
- Have the plaintext ready (from a password manager, a third-party dashboard, wherever it originates) — never typed directly into the YAML file first.
- Encrypt it immediately:
echo -n "the-value" | bns secrets encrypt --organization <ORG_ID> - Paste the resulting
ENCRYPTED[...]string directly intobunnyshell.yaml. - Commit the file — the plaintext was never written to disk in this flow at all.
- If you ever need to verify what's actually configured,
bns secrets decryptthe stored value rather than re-entering it from memory.
Team Access Patterns: Who Can Decrypt What
Because bns secrets decrypt requires organization-level API access, the practical access-control question isn't "who can see this ciphertext string" (it's meaningless without decryption access, so it can safely live in a reviewed git repository) but "who holds a token capable of decrypting it." This is exactly where scoped Personal Access Tokens matter again: a PAT scoped to a specific project with only view permission can still read a bunnyshell.yaml containing ENCRYPTED[...] values, but decrypting them requires whatever permission level your organization has configured as necessary for the secrets decrypt operation specifically. Teams handling genuinely sensitive credentials should verify explicitly which permission level is required for decrypt (rather than assuming view is sufficient) and provision tokens accordingly — a broad, unscoped account-wide token capable of decrypting everything in every project it can see is a much larger blast radius than most integrations or team members actually need.
Comparing This to a Dedicated Secrets Manager
Teams already running HashiCorp Vault, AWS Secrets Manager, or a similar dedicated secrets management system sometimes ask whether Bunnyshell's built-in encryption is meant to replace that system or coexist with it. The honest answer is coexistence, not replacement, for most setups: Bunnyshell's SECRET[...]/ENCRYPTED[...] mechanism solves the specific, narrow problem of "this value needs to live in an environment definition file without being plaintext" — it is not a general-purpose secrets management platform with features like automatic rotation policies, fine-grained per-secret access policies, or a dedicated audit UI that a system like Vault provides. A common and reasonable pattern is fetching a secret from your existing Vault instance at deploy time (via a GenericComponent deploy script that calls the Vault API) rather than storing the value in bunnyshell.yaml at all — reserving Bunnyshell's built-in encryption for values that genuinely only need to exist within the scope of environment configuration and don't warrant the operational overhead of a separate Vault policy.
Encrypting Secrets Inside CI, Not Just From a Developer Laptop
Everything demonstrated in this guide works identically when run from a CI job rather than a developer's terminal — a useful pattern for pipelines that need to provision a fresh secret programmatically (a per-run randomly generated database password, say) rather than reusing a static value across every environment:
1# .github/workflows/deploy-with-fresh-secret.yml
2- name: Generate and encrypt a fresh database password
3 run: |
4 NEW_PASSWORD=$(openssl rand -base64 24)
5 ENCRYPTED=$(echo -n "$NEW_PASSWORD" | bns secrets encrypt --organization "$ORG_ID" -o json | jq -r '.expression')
6 echo "encrypted_db_password=$ENCRYPTED" >> "$GITHUB_OUTPUT"
7 env:
8 BUNNYSHELL_TOKEN: ${{ secrets.BUNNYSHELL_TOKEN }}This pattern — generate fresh, encrypt immediately, never persist the plaintext anywhere including CI's own state — is a meaningfully stronger security posture than a single long-lived shared secret reused across every environment indefinitely, and costs almost nothing extra to implement once the encrypt-on-demand pattern is understood.
Handling a Suspected Compromised Encryption Key
A question worth having an answer to before it's urgent: if you suspect the underlying encryption mechanism itself (not an individual secret) may have been compromised — an extremely rare event, but worth a documented response plan rather than improvising one under pressure — the correct response is re-encrypting every stored secret with a freshly-rotated organization key, not simply rotating the individual plaintext values while leaving them encrypted under a potentially-compromised key. This is a materially different and more involved operation than routine per-secret rotation, and if your organization operates under a compliance framework with an incident response plan, this scenario should be explicitly represented in it — including who has the authority to trigger organization-wide re-encryption and how affected environments get redeployed with the newly-encrypted values afterward.
Testing That a Secret Actually Made It Into the Running Container
A useful sanity check after encrypting and deploying a new secret, before trusting that it's correctly wired up: confirm the value is actually present and correct inside the running container, using the same bns exec mechanism covered in SSH Into a Running Environment, rather than assuming a successful deploy pipeline implies the environment variable interpolation resolved correctly:
bns exec <component-id> -- sh -c 'echo "Length: ${#DB_PASSWORD}"'Checking the length rather than printing the value itself avoids putting the actual secret into a terminal scrollback or a CI log while still confirming something non-empty and roughly the expected size actually landed in the container's environment — a useful middle ground between "trust it worked" and "expose the plaintext to verify it."
Secrets in Ephemeral vs. Long-Lived Environments: A Different Rotation Calculus
Worth distinguishing explicitly: a secret configured in a primary environment that spawns short-lived ephemeral copies has a fundamentally different exposure profile than a secret configured in a single long-lived environment that runs for months. An ephemeral environment's secret exists for the lifetime of one PR review cycle — typically hours to days — and is destroyed along with the environment on merge/close, which is itself a form of implicit rotation, since the next PR's ephemeral environment gets a fresh instance of whatever the primary's current ENCRYPTED[...] value decrypts to, deployed fresh. A long-lived primary or demo environment's secret, by contrast, persists as long as the environment does, with no natural rotation forced by the environment's own lifecycle — which is exactly why long-lived environments need an explicit, calendar-driven rotation practice, while ephemeral ones inherit a reasonable rotation cadence almost for free, simply by virtue of how often they're recreated.
The Habit This Guide Is Really Asking For
If there's one line to remember from this entire guide, it's this: never let a plaintext credential exist in a file you didn't create specifically to be encrypted a moment later. Every recommendation above — echo -n over bare echo, pre-encrypting before writing to a tracked file, verifying stored values by decrypting rather than assuming — is a variation on that one principle, applied to the specific places a team's actual workflow tends to slip.
Why This Matters Beyond Any Single Secret
The discipline described in this guide — plaintext never touches a tracked file, encryption happens at the earliest possible moment, values are verified rather than assumed — is a small set of habits, but it's the specific set of habits that determines whether a team's environment definitions are actually safe to store in a normal, openly-reviewed git repository, or whether they require a separate, more restricted handling process that slows down every legitimate configuration change out of caution. Getting this right from the first secret a team ever encrypts means bunnyshell.yaml stays a normal, reviewable artifact indefinitely, rather than becoming a file people are nervous to open.
Related Reading
For applying this encryption pattern specifically to E2E test environments, see Securing E2E Test Environments. For verifying a decrypted secret actually reached a running container, see SSH Into a Running Environment.
Checklist
- No
bunnyshell.yamlin git history contains a bare plaintext credential -
echo -n, never bareecho, is used when piping secrets intobns secrets encrypt - Team members know the difference between
SECRET[...](encrypt-on-save) andENCRYPTED[...](pre-encrypted) and when each is appropriate - No value is ever wrapped in
SECRET[ENCRYPTED[...]]
Keep secrets out of git history, permanently.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.
Frequently Asked Questions
What is the difference between SECRET["..."] and ENCRYPTED[...] in bunnyshell.yaml?
SECRET["plaintext"] is an authoring convenience — you write the plaintext value once in the YAML file and Bunnyshell encrypts it automatically the moment the environment is created or updated, storing only the encrypted form afterward. ENCRYPTED[...] is the already-encrypted form, produced directly by bns secrets encrypt, meant for values that should never exist in plaintext in any file at all, including one that gets overwritten a moment later.
Is it safe to commit a bunnyshell.yaml containing SECRET["plaintext-value"] to git?
No — that commit contains the actual plaintext, and git history is effectively permanent even if the file is edited afterward. SECRET[...] should be used interactively (encrypted immediately on apply, never committed in that form) or, better, pre-encrypted with bns secrets encrypt before it is ever written to a tracked file.
What encryption is used under the hood?
The encryption is organization-scoped and managed by Bunnyshell's backend — the CLI commands are a thin wrapper around an API call, not a local encryption implementation, which means the same encrypted value cannot be decrypted by someone without appropriate access to your organization on Bunnyshell, even if they have the ciphertext string itself.
Can I decrypt a secret to verify what value is actually configured?
Yes, bns secrets decrypt takes an ENCRYPTED[...] string and returns the plaintext, useful for auditing what a stored value actually is when debugging a credential-related failure, rather than assuming based on when it was last set.
Does double-wrapping a value in SECRET[ENCRYPTED[...]] work?
No — this double-encrypts the value and it will fail to decrypt correctly at runtime. SECRET[...] and ENCRYPTED[...] are two separate entry points into the same underlying encrypted storage, not something to compose together.