Skip to content

Commit

Permalink
feat(config): accept CLOUDFLARE_* and all compatible token settings (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia authored Sep 28, 2024
1 parent 5111ab7 commit 4fc883c
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 133 deletions.
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

0 comments on commit 4fc883c

Please sign in to comment.