A few days ago, we published a new version of both the securestore
library/crate and the ssclient
CLI used to create, manage, and retrieve secrets from SecureStore vaults, an open and cross-language protocol for KISS secrets management. SecureStore vaults provide a more secure and far more reliable solution to storing secrets in environment variables and a simpler and less error prone alternative to network-based secrets management solutions, and make setting up development environments a breeze.
For some background, the SecureStore protocol (first published in 2017) is an open specification and cross-language library/frontend for securely storing encrypted secrets versioned in git, alongside your code. We have implementations available in rust (crate, cli) and for C#/.NET (api and cli, nuget) and the specification is purposely designed to be both easy-to-use and easy-to-port to other languages or frameworks.
This is the first update with (minor) breaking changes to the securestore
public api, although pains have been taken to ensure that most common workflows won’t break. The changes are primarily to improve ergonomics when retrieving secrets from rust, and come with completely rewritten docs and READMEs (for the project, the lib, and the cli).
Storing Secrets in GIT
We’ve previously written at length about the many reasons for preferring the SecureStore approach of storing your secrets in a human-readable format, versioned in git alongside your code. At this point, it’s probably unnecessary for us to explain why hard-coding secrets in your code and committing them to your repo is a bad idea. Similarly, unless you are deploying code to a fleet of servers from a dozen different repositories to multiple, non-interchangeable servers distributed across time and space, chances are you don’t need and shouldn’t use a networked secrets management server, thereby increasing both operational and runtime complexity, adding dependencies on external services, and worrying about managing and securing yet another bit of infrastructure in-prod. And you shouldn’t be saving secrets as environment variables where they’re hard to manage and even harder to secure against runtime leaks: environment variables were never meant to stay secret.
The SecureStore protocol lets you conveniently store secrets to an encrypted vault, and verify that your secrets and the code using them remain in sync. You can require that code isn’t merged/committed without the accompanying secrets (for dev, staging, and/or production) it uses being available (without disclosing their contents). You can unify the how secrets are stored and retrieved in-dev and in-prod, and greatly reduce the complexity of setting up a dev environment (compare the complexity of working on a codebase that has a runtime dependency on a networked PostgreSQL db vs a local SQLite one). You can see and track down or revert changes to secrets the same way you can with code, using the same tools you already know and love. You can deploy new secrets as easily as pushing to master
and letting your CI do its thing. The list goes on and on.
Using SecureStore from rust
While the securestore
crate exposes everything you need to create a SecureStore vault, add and update secrets, and retrieve them at runtime; most users will want to use the ssclient
frontend (built on the securestore
crate) to create new SecureStore vaults or to add/remove secrets, and then use the securestore
crate in their app only to retrieve secrets at runtime.
Creating a new SecureStore vault
Creating a new SecureStore vault and adding secrets is made incredibly easy with the ssclient
frontends, available as a dependency-free single executable and also installable via cargo
. In this example, we’ll create a new SecureStore vault that’s encrypted with a password but can also be decrypted headlessly at runtime with an exported keyfile:
> cargo install ssclient
> ssclient create secrets.json --export-key secrets.key
Password: ********
Confirm password: ********
> ssclient set db:username pgsql
Password: ********
> ssclient set db:password
Value: pgsql123
Password: ********
We created a new SecureStore vault, stored as a JSON-encoded, human-readable text file called secrets.json
, secured it with a password, and exported a key for headless use (secrets.key
). We then added a not-so-secret db:username
and a secret named db:password
, securely stored with their values.
We’ll commit secrets.json
to our git repository (ideally we’d commit the updated secrets.json
containing the encrypted db:username
and db:password
secrets at the same time as we commit the code that retrieves these secrets at runtime). The secrets.key
file is extremely sensitive and is never committed to git (ssclient
helpfully adds a .gitignore
rule for us to make sure that we never commit it by accident).
Retrieving secrets at runtime
In our rust app, we’ll add a dependency on the securestore
crate to Cargo.toml
and then add some code to load the SecureStore vault and retrieve the secret so we can connect to the postgres instance:
Cargo.toml:
[package]
name = "sstemp"
version = "0.1.0"
edition = "2021"
[dependencies]
once_cell = "1.13.0"
securestore = "0.100"
This example also uses the once_cell
crate to initialize a securestore::SecretsManager
singleton, but you can use whatever approach you want to abstract the access to the vault (especially if we’re only going to be reading secrets rather than updating them).
In our src/main.rs
, we’ll add some code to instantiate a SecretsManager
singleton and then to interact with that singleton and retrieve the database credentials:
use securestore::SecretsManager;
use once_cell::sync::Lazy;
static SECRETS: Lazy = Lazy::new(|| {
SecretsManager::load("secrets.json", "secrets.key")
.expect("Failed to load SecureStore vault!")
});
fn get_db_credentials() -> Result<(String, String), securestore::Error> {
let username = SECRETS.get("db:username")?;
let password = SECRETS.get("db:password")?;
Ok( (username, password) )
}
fn main() {
let credentials = get_db_credentials().unwrap();
assert_eq!(credentials.0, String::from("pgsql"));
assert_eq!(credentials.1, String::from("pgsql123"));
// TODO: Actually connect to the database with these credentials
}
You can refer to the project README for a more complete and annotated example of using SecureStore from rust, how to separate dev, staging, and production secrets, what the encrypted secrets.json
vault looks like and contains, and more.
Notable changes in securestore
and ssclient
0.100
A complete changelog for this release is available on GitHub, but for our existing securestore
users, here’s a rundown on what’s new and improved in this release and what you should look out for when migrating your existing code, in no particular order:
- As mentioned, all the documentation has been overhauled extensively. This includes the command line help for
ssclient
and the crate docs forsecurestore
, as well as all the accompanying README files and more. SecretsManager::new()
no longer takes a filename, you specify that out-of-band via the newSecretsManager::save_as()
(the oldsave()
function is still there).SecretsManager::get()
is no longer generic; the old, generic function is still there, but renamed toSecretsManager::get_as::<T>()
. This lets you omit the return type for the 99.99% of the cases where you have aString
secret.- There are some quality-of-life improvements to the
BinarySerializable
andBinaryDeserializable
traits for all two of you storing arbitrary types as SecureStore secrets. ssclient
is now git-aware and excludes exported or generated key files in.gitignore
by default (support for other VCS is coming; pull requests are welcome).ssclient
can now decrypt binary secrets and represent them as text (encoded as base64).- There is a new PEM-like keyfile format, generated by default by new versions of
securestore
andssclient
. The old binary format is still (and will remain) supported. Older versions ofsecurestore
cannot read the new ASCII-armored key format. - You should use
KeySource::from_file(..)
instead ofKeySource::File(..)
(usage is the same).KeySource::File(..)
may be deprecated in the future (it’s no longer an actual enum variant but rather a function masquerading as an enum variant for backwards compatibility). Refer to the changelog to understand why.