Credentials + Secrets
Here's how we store credentials and secrets in Z. It's simple by design, and an extension of the behaviour that we see in a default Phoenix project.
- All modules and libraries that reference API keys and similar secrets will store those references into config variables eg.
Application.get_env(:z, MyModule).secret_api_key
. This keeps us from hand-rolling config lookups into a million different modules and consolidates into the config files where that stuff belongs. - In production, all secrets are stored in environment variables and accessed with
System.get_env()
in the fileconfig/runtime.exs
. - We're leveraging Elixir's new "runtime" configuration setup - more on how that works in the
Config
docs as well as this video by Jose Valim explaining the feature when it was launched.- Within
config/runtime.exs
, an error should be raised if the environment variable isn't present, because that will cause a lot of bugs. We have created a simpleenv.(key, message)
function to make this dead simple.
- Within
- In test and development, all secrets are stored in
config/test.secret.exs
andconfig/dev.secret.exs
and loaded on a per-environment basis. Those keys are hard-coded into those files for dev/test, which is why we don't store those files in version control. Similar to Rails' "master key", you'll have to get this from somebody who has it :) You will receive bright red warnings if you lack these files when doing anything, so it'll be hard to forget.
Here's an example API configuration from config/runtime.exs
. Note that a missing HIVE_API_URL
doesn't raise an error, since the default is fine for all intents + purposes.
config :hive_service,
hive_api_token: env.("HIVE_API_TOKEN", "")
Here's how that same example might look in config/dev.secret.exs
:
config :hive_service, [
hive_api_token: "yer token here"
]
In this case, the same code might be used for config/test.secret.exs
or we might decide to not use the HIVE API in testing, in which case we wouldn't need to put anything there.
Caveats
Secret Drift for Development
Cleary, this setup could cause a situation where developers' personal config/*secret.exs
files get out of sync. So we keep the latest version of this file in our shared password app.
Private Dependency Management
Another caveat to this system is the credential storage for internal EXPLO libraries - hive_monitor
, hive_service
, and explo_comm
for example. Since Gigalixir (our host) doesn't have the capability of sharing SSH public key from the deployment server, we have to put a username/password into our source, so that the deployment server can clone the relevant git repositories. We used the suggested method (look towards the end of the linked docs section) of encoding the username/password as an environment variable in mix.exs. The problem with this approach, as the document points out, is that the password is encoded in mix.lock.
So, to mitigate the damage that finding that username/password combo in mix.lock could cause, we've ensured that none of those libraries are storing credentials in source code any more... Then we made them public on GitHub:
API Tokens
Design
This is good: https://dockyard.com/blog/2020/01/14/tips-for-tokens , and it's generally the advice we should use if we ever want to store user tokens in Z. See also https://hexdocs.pm/phoenix/Phoenix.Token.html which is an excellent built-in resource.
However, for API purposes we have different needs:
- It's good if the token is associated with the user of the token i.e. can be paired with a provenance.
- The token needs to be stored as an environment variable for security, not in the database.
So the solution is a simple text-based token language that looks like this:
# name:token pairs, separated by commas, no whitespace.
API_TOKENS="portico:qed456,lynton:abc123"
Requests
The ZWeb.Plugs.APIAuthentication
plug will handle API authentication by checking the request header for an {"authentication", "bearer #{TOKEN}"}
header, where TOKEN
is the actual token. The request will be logged against the user contained before the ":" in the matching environment token.
We have a similar ZWeb.Plugs.QueryTokenAuthentication
plug to handle endpoints that need to be hit by humans, via URL, with auto-login (ie query token auth).
Research, Notes, and Further Reading
The primary resources for how configuration and environments work in Elixir are in the module docs:
- https://hexdocs.pm/elixir/Application.html#content
- https://hexdocs.pm/elixir/Config.html#content
- https://hexdocs.pm/mix/Mix.Config.html#content (deprecated, but still in use as of Feb. 2021)
- https://elixir-lang.org/blog/2020/10/06/elixir-v1-11-0-released/ has a section about the new config/runtime.exs convention which is helpful in some more advanced deployment scenarios.
Those are all pretty much required reading ☝ . In addition, this blog post gives a really helpful overview of the above: https://blog.nytsoi.net/2020/05/05/elixir-time-for-configuration .
This post is really helpful, but outdated since newer versions of Elixir have different configuration settings: https://blog.plataformatec.com.br/2016/05/how-to-config-environment-variables-with-elixir-and-exrm/
Looking at solutions like https://github.com/findmypast/vaultex , it strikes me that we'd still have to store some sort of configuration variable (authentication method) in an environment variable OR we'd have to deal with managing SSH credentials or something. This approach seems like the worst of both worlds at our scale.
I did a branch where I implemented this: https://github.com/kieraneglin/encrypted_secrets_ex , but I didn't like that it keeps secrets in memory while the app is running. I also read through the documentation for https://github.com/Nebo15/confex which solves a lot of the problems of using System.get_env() AND/OR Application.get_env() with fallbacks, but is overkill for our needs.