Manage your Cloudflare credentials, securely.
The goal of this project is to ensure that when you need to interact with Cloudflare:
- You're not storing credentials unencrypted; and
- You're not exposing your credentials to the entire environment or to all processes; and
- You're not using long lived credentials
To achieve this, cf-vault uses the concept of profiles with associated scopes
to either generate short lived API tokens or retrieve the API key from secure
storage (such as Mac OS keychain).
$ brew tap jacobbednarz/tap
$ brew install jacobbednarz/tap/cf-vault
-
First step is to generate a new API key or API token. Either will work however there are some subtle differences to take into consideration before choosing your path.
- API tokens are not supported by all services yet. Regardless of whether you are using the short lived credentials or long lived token, it may not work for all services and you may need to have a backup profile defined using an API key to cover all scenarios .
- API keys are supported everywhere however they cannot be scoped. API keys have the permission and scopes that your user account has. This can be dangerous so be sure to tread carefully as it may have unintended consequences.
While it is possible (and better practice of principle of least privilege), to use an API token with only permissions to create a new API token, this isn't really viable for all use cases yet. The recommended approach is to use the API key for the profile and rely on a custom policy to scope the short lived credential. This allows the best of both worlds where if you need to use a service that doesn't support API tokens, you don't need to create a new profile.
To create a new API token:
> https://dash.cloudflare.com/ > My Profile > API Tokens > Create API tokenTo retrieve your API key:
> https://dash.cloudflare.com/ > My Profile > API Tokens > Global API Key -
If you're using an API key, you can skip to the next step. Otherwise, navigate through the UI and configure what permissions and resources you'd like to assign to the token. If you're looking to use an API token to generate short lived API tokens, you should only need the single predefined "Create API tokens" permission. See the section below on generating the desired TOML output for instructions on how to do automatically convert policies from API responses.
Note: Be sure to note down the API token before closing/navigating away from the UI as you won't be able to retrieve it again.
-
Once you have your API key or API token value, you can start using
cf-vaultby creating a profile. A profile is the collection of configuration that tellscf-vaulthow you intend to interact with the Cloudflare credentials. You need to start by callingcf-vault add [your-profile-name]where[your-profile-name]is a label for what the credential/use of the profile is. Some examples:cf-vault add write-everythingcf-vault add read-onlycf-vault add super-scary-access-everythingcf-vault add api-token-to-create-other-tokens
There is no limit on how many profiles you have if you prefer to have specific profiles for your use cases. Adding a profile with a name that already exists fails rather than replacing it; pass
--forceto overwrite it. Overwriting also removes the credential the old profile stored, from the keychain or the encryptedsecrets/<profile>.agefile, when the new one is stored somewhere else. -
Now that you have created a profile, you can use it with
cf-vault exec [your-profile-name].
If you do not wish to use the short lived credentials functionality,
that's totally fine and you can do so by omitting the session_duration value
and instead the long lived credentials you've setup will be used.
cf-vault allows you to manage your Cloudflare credentials in a safe place and
only expose the credentials to the processes that require them and only for a
limited timespan.
$ env | grep -i cloudflare
# => no results
$ cf-vault exec work -- env | grep -i cloudflare
CLOUDFLARE_VAULT_SESSION=work
CLOUDFLARE_EMAIL=jacob@example.com
CLOUDFLARE_API_KEY=s3cr3t
CF_EMAIL=jacob@example.com
CF_API_KEY=s3cr3tTo reference the credentials in the command's arguments, wrap the command in a
shell using single quotes. Without them, your current shell expands the
variables before cf-vault has populated them and the command receives empty
(or stale) values.
$ cf-vault exec work -- sh -c 'curl -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" https://api.cloudflare.com/client/v4/user/tokens/verify'If you don't provide a command, you will be dropped into a new shell with the credentials populated.
$ cf-vault exec work
$ env | grep -i cloudflare
CLOUDFLARE_VAULT_SESSION=work
CLOUDFLARE_EMAIL=jacob@example.com
CLOUDFLARE_API_KEY=s3cr3t
CF_EMAIL=jacob@example.com
CF_API_KEY=s3cr3t
$ exit
$ env | grep -i cloudflare
# => no resultscf-vault remove (or cf-vault rm) deletes a profile from the config along
with its stored credential, whether that is in the keychain, the file keyring
or an encrypted secrets/<profile>.age file. It asks for confirmation first,
and --force skips the prompt. Outside a terminal, where there is nobody to
ask, --force is required.
$ cf-vault remove work
Remove profile "work" and its stored credential? [y/N]: y
Removed profile "work". The credential is still valid at Cloudflare; revoke it in the dashboard if it is no longer needed.Removing a profile only forgets the credential locally. Revoke the API token, or roll the global API key, in the Cloudflare dashboard to stop it working.
cf-vault add prompts for the authentication value when run in a terminal,
then for the email address if the value is a global API key. For scripts and
CI, provide both up front instead and nothing is prompted for:
- The authentication value is read from stdin with
--authentication-value-stdin, or from theCF_VAULT_AUTH_VALUEenvironment variable. When both are provided, stdin wins. The flag expects the value to be piped in and fails when stdin is a terminal, where typing it would echo it back. - The email address is passed with
--email. It is required for global API keys and not needed for API tokens.
$ printf '%s' "$CLOUDFLARE_API_TOKEN" | cf-vault add ci --authentication-value-stdin
$ CF_VAULT_AUTH_VALUE="$CLOUDFLARE_API_KEY" cf-vault add ci --email jacob@example.comThere is intentionally no flag that takes the authentication value directly.
Command line arguments are visible to other users on the machine via ps, and
end up in shell history and CI logs.
add fails instead of prompting when stdin isn't a terminal and neither
source is provided, or when a global API key is given without --email. Some
secret storage still needs a person present regardless of these options: the
macOS Keychain may ask for permission, and the hardware-backed
credentials always need a physical touch. On
headless machines, use the file backend (CF_VAULT_BACKEND=file) with
CF_VAULT_FILE_PASSPHRASE set.
Instead of protecting stored credentials with your macOS login password, you can encrypt them to a hardware key — Touch ID via the Secure Enclave, or a YubiKey. Unlocking then requires a fingerprint or a physical touch instead of a password.
This is opt-in per profile via a flag on add. Existing profiles are
unaffected.
brew install age age-plugin-se
cf-vault add my-profile --secure-enclave
A Secure Enclave key is generated on first use. The key never leaves this Mac — losing the machine means losing access to any credential encrypted with it.
brew install age age-plugin-yubikey
age-plugin-yubikey --generate # one-time key enrollment
cf-vault add my-profile --yubikey
cf-vault uses whichever identity is currently enrolled on the plugged-in
YubiKey. The key must be plugged in for cf-vault exec to succeed.
If you don't need to generate a custom token policy, you can instead use one of
the predefined templates which takes care of the heavy lifting for you. You can
use read-only (read all resources) or write-everything (write all resources)
as the --profile-template flag and it will generate everything needed behind
the scenes on your behalf. The templates only apply to short lived tokens, so
--profile-template and --session-duration must be passed together. The
session duration is a Go duration such as 15m or 1h, must be at least 10s
so the token doesn't expire before the command gets to use it, and is rounded
down to whole seconds.
Examples:
cf-vault add my-read-profile-name --profile-template "read-only" --session-duration "15m"cf-vault add my-write-profile-name --profile-template "write-everything" --session-duration "15m"
By default the templates apply to every account and zone you have access to.
Pass --account-id and/or --zone-id (repeatable, or comma separated) to
narrow the generated policies:
| Flags | Account permissions | Zone permissions |
|---|---|---|
| neither | all accounts | all zones |
--account-id |
listed accounts | all zones in the listed accounts |
--zone-id |
none | listed zones |
--account-id and --zone-id |
listed accounts | listed zones |
Restricting to zones alone drops the account permissions entirely so the token doesn't retain account-wide access. User permissions are always scoped to yourself.
Examples:
cf-vault add my-account-profile --profile-template "write-everything" --session-duration "15m" --account-id "01a7362d577a6c3019a474fd6f485823"cf-vault add my-zone-profile --profile-template "read-only" --session-duration "15m" --zone-id "023e105f4ecef8ad9ca31a8372d0c353" --zone-id "353c0d2738a13ac9da8fece4f501e320"
Account API tokens belong to an account rather than a user, so they keep
working after the person who created them leaves. Add one by passing the ID
of the account that owns it with --owner-account-id, or type it in when
add asks for it after a cfat_ token:
$ printf '%s' "$TOKEN" | cf-vault add ci --authentication-value-stdin --owner-account-id 01a7362d577a6c3019a474fd6f485823The ID is saved to the profile as owner_account_id, and short lived tokens
are then created in that account instead of for your user. An account token
can only reach its own account, so with --profile-template:
- the account permissions and all zones default to the owning account instead of every account you have access to;
--account-idmay only name the owning account; and- there are no user permissions, as there is no user.
The ID is the only thing that makes a profile an account token profile, and
cf-vault list shows such profiles with an account owner. A cfat_ token
can't be added without it, and a cfut_ token is refused with the flag.
Legacy tokens don't say who owns them, so without the flag they are treated
as a user's.
While TOML is more readable, its not always straight forward to generate the desired output. Instead, you can use the Cloudflare dashboard to build the policy you'd like and then covert that to TOML using some tooling to avoid manually building your policy (though you can if you understand the syntax!).
-
Using
cf-vault addcreate your profile following the prompts. -
Create the token you'd like to use on the command line using the Cloudflare dashboard.
-
Make the API call to fetch the token you've just created. See https://developers.cloudflare.com/api/operations/user-api-tokens-token-details or https://developers.cloudflare.com/api/operations/user-api-tokens-list-tokens to fetch all tokens.
-
Write the contents of the single
resultJSON payload to a local file. For the example, I'll useexample_token.jsonfor the documentation. -
Run the following command using
dockerwhich will pull thego-tomltool for coverting JSON -> TOML. Remember to replaceexample_token.jsonwith your filename.docker run -v $PWD:/workdir pelletier/go-toml jsontoml /workdir/example_token.json -
Paste the generated
policyinto your configuration file. You will need to adjust the structure slightly to match the hierarchy. For instance, if I have the following profile:
[profiles]
[profiles.doco-example]
auth_type = "api_token"
email = "me@example.com"
session_duration = "15m"and my policy output:
[[policies]]
effect = "allow"
[[policies.permission_groups]]
id = "eb258a38ea634c86a0c89da6b27cb6b6"
name = "Access: Apps and Policies Read"
[[policies.permission_groups]]
id = "517b21aee92c4d89936c976ba6e4be55"
name = "Zone Settings Read"
[[policies.permission_groups]]
id = "c8fed203ed3043cba015a93ad1616f1f"
name = "Zone Read"
# .. snipThe policy needs to be updated to prepend profiles.doco-example to the
section keys.
[[profiles.doco-example.policies]]
effect = "allow"
[[profiles.doco-example.policies.permission_groups]]
id = "eb258a38ea634c86a0c89da6b27cb6b6"
name = "Access: Apps and Policies Read"
[[profiles.doco-example.policies.permission_groups]]
id = "517b21aee92c4d89936c976ba6e4be55"
name = "Zone Settings Read"
[[profiles.doco-example.policies.permission_groups]]
id = "c8fed203ed3043cba015a93ad1616f1f"
name = "Zone Read"
# .. snipMaking the complete configuration look like:
[profiles]
[profiles.doco-example]
auth_type = "api_token"
email = "me@example.com"
session_duration = "15m"
[[profiles.doco-example.policies]]
effect = "allow"
[[profiles.doco-example.policies.permission_groups]]
id = "eb258a38ea634c86a0c89da6b27cb6b6"
name = "Access: Apps and Policies Read"
[[profiles.doco-example.policies.permission_groups]]
id = "517b21aee92c4d89936c976ba6e4be55"
name = "Zone Settings Read"
[[profiles.doco-example.policies.permission_groups]]
id = "c8fed203ed3043cba015a93ad1616f1f"
name = "Zone Read"
# .. snip