Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): accept CLOUDFLARE_* and all compatible token settings #948

Merged
merged 3 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ _(Click to expand the following items.)_
```bash
docker run \
--network host \
-e CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
-e CLOUDFLARE_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
-e DOMAINS=example.org,www.example.org,example.io \
-e PROXIED=true \
favonia/cloudflare-ddns:latest
Expand All @@ -95,7 +95,7 @@ docker run \
You need the [Go tool](https://golang.org/doc/install) to run the updater from its source.

```bash
CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
CLOUDFLARE_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
DOMAINS=example.org,www.example.org,example.io \
PROXIED=true \
go run github.com/favonia/cloudflare-ddns/cmd/ddns@latest
Expand Down Expand Up @@ -132,7 +132,7 @@ services:
security_opt: [no-new-privileges:true]
# Another protection to restrict superuser privileges (optional but recommended)
environment:
- CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN
- CLOUDFLARE_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN
# Your Cloudflare API token
- DOMAINS=example.org,www.example.org,example.io
# Your domains (separated by commas)
Expand All @@ -143,15 +143,14 @@ services:
_(Click to expand the following important tips.)_

<details>
<summary>🔑 <code>CF_API_TOKEN</code> is your Cloudflare API token</summary>
<summary>🔑 <code>CLOUDFLARE_API_TOKEN</code> is your Cloudflare API token</summary>

The value of `CF_API_TOKEN` should be an API **token** (_not_ an API key), which can be obtained from the [API Tokens page](https://dash.cloudflare.com/profile/api-tokens). (The less secure API key authentication is deliberately _not_ supported.)
The value of `CLOUDFLARE_API_TOKEN` should be an API **token** (_not_ an API key), which can be obtained from the [API Tokens page](https://dash.cloudflare.com/profile/api-tokens). The less secure API key authentication is deliberately _not_ supported.

- To update only DNS records, use the **Edit zone DNS** template to create a token.
- To update only WAF lists, choose **Create Custom Token** and then add the **Account - Account Filter Lists - Edit** permission to create a token.
- To update DNS records _and_ WAF lists, use the **Edit zone DNS** template and then add the **Account - Account Filter Lists - Edit** permission when creating the token.

You can also adjust the permissions of existing tokens at any time!
- To update _both_ DNS records _and_ WAF lists, use the **Edit zone DNS** template and then add the **Account - Account Filter Lists - Edit** permission when creating the token.
- You can adjust the permissions of existing tokens at any time!

</details>

Expand Down Expand Up @@ -259,15 +258,20 @@ _(Click to expand the following items.)_
<details>
<summary>🔑 The Cloudflare API token</summary>

> Exactly one of the following variables should be set.
> Starting with version 1.15.0, the updater supports environment variables that begin with `CLOUDFLARE_*`. Multiple environment variables can be used at the same time, provided they all specify the same token.

| Name | Meaning |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `CF_API_TOKEN` | The [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API |
| `CF_API_TOKEN_FILE` | A path to a file that contains the [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API |
| Name | Meaning |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `CLOUDFLARE_API_TOKEN` | The [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API |
| `CLOUDFLARE_API_TOKEN_FILE` | A path to a file that contains the [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API |
| `CF_API_TOKEN` (will be deprecated in version 2.0) | Same as `CLOUDFLARE_API_TOKEN` |
| `CF_API_TOKEN_FILE` (will be deprecated version in 2.0) | Same as `CLOUDFLARE_API_TOKEN_FILE` |

- 🔑 To update DNS records, the updater needs the **Account - Account Filter Lists - Edit** permission.
- 🔑 To manipulate WAF lists, the updater needs the **Zone - DNS - Edit** permission.
> 🚂 Cloudflare is updating its tools to use environment variables starting with `CLOUDFLARE_*` instead of `CF_*`. It is recommended to align your setting to align with this new convention. However, the updater will fully support both `CLOUDFLARE_*` and `CF_*` environment variables until version 2.0.
>
> 🔑 To update DNS records, the updater needs the **Account - Account Filter Lists - Edit** permission.
>
> 🔑 To manipulate WAF lists, the updater needs the **Zone - DNS - Edit** permission.

</details>

Expand Down Expand Up @@ -423,8 +427,8 @@ _(Click to expand the following items.)_

| Old Parameter | | Note |
| -------------------------------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `API_KEY=key` | ✔️ | Use `CF_API_TOKEN=key` |
| `API_KEY_FILE=file` | ✔️ | Use `CF_API_TOKEN_FILE=file` |
| `API_KEY=key` | ✔️ | Use `CLOUDFLARE_API_TOKEN=key` |
| `API_KEY_FILE=file` | ✔️ | Use `CLOUDFLARE_API_TOKEN_FILE=file` |
| `ZONE=example.org` and `SUBDOMAIN=sub` | ✔️ | Use `DOMAINS=sub.example.org` directly |
| `PROXIED=true` | ✔️ | Same (`PROXIED=true`) |
| `RRTYPE=A` | ✔️ | Both IPv4 and IPv6 are enabled by default; use `IP6_PROVIDER=none` to disable IPv6 |
Expand All @@ -441,7 +445,7 @@ _(Click to expand the following items.)_

| Old JSON Key | | Note |
| ------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cloudflare.authentication.api_token` | ✔️ | Use `CF_API_TOKEN=key` |
| `cloudflare.authentication.api_token` | ✔️ | Use `CLOUDFLARE_API_TOKEN=key` |
| `cloudflare.authentication.api_key` | ❌ | Please use the newer, more secure [API tokens](https://dash.cloudflare.com/profile/api-tokens) |
| `cloudflare.zone_id` | ✔️ | Not needed; automatically retrieved from the server |
| `cloudflare.subdomains[].name` | ✔️ | Use `DOMAINS` with [**fully qualified domain names (FQDNs)**](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) directly; for example, if your zone is `example.org` and your subdomain is `sub`, use `DOMAINS=sub.example.org` |
Expand Down
6 changes: 4 additions & 2 deletions internal/config/config_read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
func unsetAll(t *testing.T) {
t.Helper()
unset(t,
"CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE",
"CF_API_TOKEN", "CF_API_TOKEN_FILE", "CF_ACCOUNT_ID",
"IP4_PROVIDER", "IP6_PROVIDER",
"DOMAINS", "IP4_DOMAINS", "IP6_DOMAINS", "WAF_LISTS",
Expand All @@ -43,7 +44,7 @@ func TestReadEnvWithOnlyToken(t *testing.T) {
mockCtrl := gomock.NewController(t)

unsetAll(t)
store(t, "CF_API_TOKEN", "deadbeaf")
store(t, "CLOUDFLARE_API_TOKEN", "deadbeaf")

var cfg config.Config
mockPP := mocks.NewMockPP(mockCtrl)
Expand Down Expand Up @@ -79,7 +80,8 @@ func TestReadEnvEmpty(t *testing.T) {
mockPP.EXPECT().IsShowing(pp.Info).Return(true),
mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Reading settings . . ."),
mockPP.EXPECT().Indent().Return(innerMockPP),
innerMockPP.EXPECT().Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE"),
innerMockPP.EXPECT().Noticef(pp.EmojiUserError,
"Needs either %s or %s", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE"),
)
ok := cfg.ReadEnv(mockPP)
require.False(t, ok)
Expand Down
128 changes: 104 additions & 24 deletions internal/config/env_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,128 @@ import (

var oauthBearerRegex = regexp.MustCompile(`^[-a-zA-Z0-9._~+/]+=*$`)

func readAuthToken(ppfmt pp.PP) (string, bool) {
var (
token = Getenv("CF_API_TOKEN")
tokenFile = Getenv("CF_API_TOKEN_FILE")
)
var ok bool
// Keys of environment variables.
const (
TokenKey1 string = "CLOUDFLARE_API_TOKEN" //nolint:gosec
TokenKey2 string = "CF_API_TOKEN" //nolint:gosec
TokenFileKey1 string = "CLOUDFLARE_API_TOKEN_FILE" //nolint:gosec
TokenFileKey2 string = "CF_API_TOKEN_FILE" //nolint:gosec
)

// HintAuthTokenNewPrefix contains the hint about the transition from
// CF_* to CLOUDFLARE_*.
const HintAuthTokenNewPrefix string = "Cloudflare is transitioning its tools to use the prefix CLOUDFLARE instead of CF. To align with this change, it is recommended to use CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_TOKEN_FILE) instead of CF_API_TOKEN (or CF_API_TOKEN_FILE) moving forward. All options will be fully supported until version 2.0." //nolint:lll,gosec

func readPlainAuthTokens(ppfmt pp.PP) (string, string, bool) {
token1 := Getenv(TokenKey1)
token2 := Getenv(TokenKey2)

var token, tokenKey string
switch {
case token1 == "" && token2 == "":
return "", "", true
case token1 != "" && token2 != "" && token1 != token2:
ppfmt.Noticef(pp.EmojiUserError,
"The values of %s and %s do not match; they must specify the same token", TokenKey1, TokenKey2)
return "", "", false
case token1 != "":
token, tokenKey = token1, TokenKey1
case token2 != "":
ppfmt.Hintf(pp.HintAuthTokenNewPrefix, HintAuthTokenNewPrefix)
token, tokenKey = token2, TokenKey2
}

// foolproof checks
// foolproof check: the sample value in README
if token == "YOUR-CLOUDFLARE-API-TOKEN" {
ppfmt.Noticef(pp.EmojiUserError, "You need to provide a real API token as CF_API_TOKEN")
ppfmt.Noticef(pp.EmojiUserError, "You need to provide a real API token as %s", tokenKey)
return "", "", false
}

return token, tokenKey, true
}

func readAuthTokenFile(ppfmt pp.PP, key string) (string, bool) {
tokenFile := Getenv(key)
if tokenFile == "" {
return "", true
}

token, ok := file.ReadString(ppfmt, tokenFile)
if !ok {
return "", false
}

if token == "" {
ppfmt.Noticef(pp.EmojiUserError, "The file specified by %s does not contain an API token", key)
return "", false
}

return token, true
}

func readAuthTokenFiles(ppfmt pp.PP) (string, string, bool) {
token1, ok := readAuthTokenFile(ppfmt, TokenFileKey1)
if !ok {
return "", "", false
}

token2, ok := readAuthTokenFile(ppfmt, TokenFileKey2)
if !ok {
return "", "", false
}

switch {
case token1 != "" && token2 != "" && token1 != token2:
ppfmt.Noticef(pp.EmojiUserError,
"The files specified by %s and %s have conflicting tokens; their content must match", TokenFileKey1, TokenFileKey2)
return "", "", false
case token1 != "":
return token1, TokenFileKey1, true
case token2 != "":
ppfmt.Hintf(pp.HintAuthTokenNewPrefix, HintAuthTokenNewPrefix)
return token2, TokenFileKey2, true
default:
return "", "", true
}
}

func readAuthToken(ppfmt pp.PP) (string, bool) {
tokenPlain, tokenPlainKey, ok := readPlainAuthTokens(ppfmt)
if !ok {
return "", false
}

tokenFile, tokenFileKey, ok := readAuthTokenFiles(ppfmt)
if !ok {
return "", false
}

var token string
switch {
case token != "" && tokenFile != "":
ppfmt.Noticef(pp.EmojiUserError, "Cannot have both CF_API_TOKEN and CF_API_TOKEN_FILE set")
case tokenPlain != "" && tokenFile != "" && tokenPlain != tokenFile:
ppfmt.Noticef(pp.EmojiUserError,
"The value of %s does not match the token found in the file specified by %s; they must specify the same token",
tokenPlainKey, tokenFileKey)
return "", false
case token != "":
case tokenPlain != "":
token = tokenPlain
case tokenFile != "":
token, ok = file.ReadString(ppfmt, tokenFile)
if !ok {
return "", false
}

if token == "" {
ppfmt.Noticef(pp.EmojiUserError, "The token in the file specified by CF_API_TOKEN_FILE is empty")
return "", false
}
token = tokenFile
default:
ppfmt.Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE")
ppfmt.Noticef(pp.EmojiUserError, "Needs either %s or %s", TokenKey1, TokenFileKey1)
return "", false
}

if !oauthBearerRegex.MatchString(token) {
ppfmt.Noticef(pp.EmojiUserWarning, "The API token does not look like a valid OAuth2 bearer token")
ppfmt.Noticef(pp.EmojiUserWarning,
"The API token appears to be invalid; it does not follow the OAuth2 bearer token format")
}

return token, true
}

// ReadAuth reads environment variables CF_API_TOKEN, CF_API_TOKEN_FILE, and CF_ACCOUNT_ID
// and creates an [api.CloudflareAuth].
// ReadAuth reads environment variables CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_TOKEN_FILE,
// CF_API_TOKEN, CF_API_TOKEN_FILE, and CF_ACCOUNT_ID and creates an [api.CloudflareAuth].
func ReadAuth(ppfmt pp.PP, field *api.Auth) bool {
token, ok := readAuthToken(ppfmt)
if !ok {
Expand Down
Loading
Loading