Working with secrets
In this section we cover how to efficiently handle secrets in environment configuration files using safe-env.
Overview
A set of custom OmegaConf resolvers is included to work with secrets in a secure way:
se.call- allows to invoke any Python callablese.auth- shortcut to invoke classes generating credentials for authentication to various sourcesse.cache- shortcut to invoke classes providing caching capabilities
It is important to highlight, that all resolvers are implemented in a way that parent config element is used as a container that stores configurations on how callable will be invoked.
Here is a sample configuration file showing how these resolvers work together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
Running se resolve shows how this configuration will be resolved with values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
Loading secrets
Depending on project specifics, secrets may need to be loaded from different sources. For example, if the project is relying on Microsoft Azure cloud infrastructure, secrets may be loaded from Azure KeyVault or directly from Azure resources (for example, access keys to specific resources). safe-env supports such scenarios by allowing to invoke arbitrary python code, what gives unlimited integration possibilities.
To invoke custom code from configuration file, se.call resolver should be used:
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
As argument se.call expects full name of a callable, or registered "known" shortcut name. In this case, get_azure_key_vault_secrets is known shortcut name for safe_env.resolvers.callables_azure.get_azure_key_vault_secrets:
def get_azure_key_vault_secrets(
url: str,
credential: Any,
names: List[str]
) -> Dict[str, Any]:
# ...
safe-env comes with a set of "known" shortcut names, which are registered via:
# ...
KNOWN_CALLABLES = {
"get_azure_key_vault_secrets": get_azure_key_vault_secrets,
"get_keyring_secrets": get_keyring_secrets,
"get_azure_rest_resource": get_azure_rest_resource,
"get_azure_devops_pat": get_azure_devops_pat
}
# ...
get_azure_key_vault_secrets expects that three arguments are provided: url, credential and names of secrets. se.call allows to provide these arguments via args and kwargs attributes under the same parent in configuration file:
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
This callable will return a dictionary with names/values for each secret name. However, this dictionary is not a valid OmegaConf container. as_container: True attribute tells se.call that we want to convert returned object to a valid container. This allows us to use values from it as:
AZ_ACCOUNT_KEY: ${kv_secrets.value.AZSTORAGEACCOUNTKEY}
selector attribute:
# ...
value: ${se.call:some_method}
selector: "<JMESPath expression to apply on returned value>"
# ...
Finally, in more complex scenarios, we may want se.call to initialize an instance of the class first, and then invoke specific method from this class. This can be achieved by using the following attributes:
# ...
value: ${se.call:<full name of the class>} # full name of the class as a callable passed to se.call
init_params: # parameters passed to constructor
args: # list of args for constructor
kwargs: # dictionary with kwargs for constructor
method: # name of the method to invoke from class instance
args: # list of args for method
kwargs: # dictionary with kwargs for method
# ...
Caching
Typically, secrets do not change every day. That's why, to improve developer experience, it may be good to cache them in a secure local storage. Good common choice is to use local keyring - the same storage, that operating system uses to store user secrets.
safe-env allows to cache retrieved secrets via cache attribute:
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
The attribute allows to specify one or multiple caches as a dictionary. If multiple caches are configured, they will be treated as being ordered alphabetically by dictionary key.
For each cache we must specify:
name- the name that will be used to store secret in the cacheprovider-se.cacheresolver with a full name of a callable, or registered "known" shortcut cache name.
In this case, keyring is a known shortcut name for safe_env.cache_providers.AzureKeyVaultSecretCache:
# ...
from .base_cache_provider import BaseCacheProvider
class KeyringCache(BaseCacheProvider):
def __init__(self, keyring_type: str = None, service_name: str = None, **kwargs):
super().__init__(**kwargs)
self.keyring_type = keyring_type
self.default_service_name = service_name
# ...
safe-env comes with a set of "known" shortcut cache names, which are registered via:
# ...
KNOWN_CACHE_CLASSES = {
"memory": cache_providers.MemoryCache,
"keyring": cache_providers.KeyringCache,
"azure.keyvault": cache_providers.AzureKeyVaultSecretCache
}
# ...
Constructor of cache provider class may expect, that specific arguments are provided. For example, KeyringCache expects that service_name is provided. Same as with se:call this can be done via:
# ...
init_params: # parameters passed to constructor
args: # list of args for constructor
kwargs: # dictionary with kwargs for constructor
# ...
safe-env comes with few commands / options that help to manage caches:
$ se flush # Delete values stored in all caches for specified environments.
$ se (resolve | activate | run) (--force-reload | -f) # Ignore all cached values and reload from sources.
$ se (resolve | activate | run) (--no-cache | -n) # Do not use caches to load/save values.
However, some caches are "technical" and should always be used - for example, in-memory cache for storing authentication credentials. To achieve this, we can mark specific cache as required:
9 10 11 12 13 14 15 16 17 18 19 | |
Authentication
Usually, retrieving secrets requires some form of authentication. For example, if secrets are stored in Azure, the user is required to authenticate via az login or interactive browser login.
safe-env allows to invoke a callable to retrieve authentication credentials via se.auth resolver:
9 10 11 12 13 14 15 16 17 18 19 | |
As argument se.auth expects a full name of a callable, or registered "known" shortcut auth provider name. In this case, azure.interactive is a known shortcut name for azure.identity.InteractiveBrowserCredential.
safe-env comes with a set of "known" shortcut auth provider names, which are registered via:
# ...
KNOWN_AUTH_CLASSES = {
"azure.default": DefaultAzureCredential,
"azure.cli": AzureCliCredential,
"azure.interactive": InteractiveBrowserCredential,
"azure.managedidentity": ManagedIdentityCredential,
"azure.devicecode": DeviceCodeCredential,
"azure.vscode": VisualStudioCodeCredential,
"azure.token": get_azure_credential_token
}
# ...
Arguments to auth provider can be provided via:
# ...
args: # list of args
kwargs: # dictionary with kwargs
# ...
Since the same credentials might be used multiple times, while resolving configuration file, it is a good idea to cache them in-memory:
9 10 11 12 13 14 15 16 17 18 19 | |
Interesting Fact
Technically se.auth is not a regular callable, but safe_env.resolvers.delayedcallable.DelayedCallable. It is invoked only when authentication credentials are really needed. For example, if secrets can be retrieved from the cache, se.auth will not ask the user for authentication credentials.
Congratulations! Now you know all moving parts, and are ready to integrate safe-env with custom secret sources, authentication providers, or caches by implementing plugins.