Skip to content

Commit

Permalink
Add possibility to pass secrets via files (#31)
Browse files Browse the repository at this point in the history
* Add possibility to pass secrets via files

Resolves #30

* Stick to 'secret' terminology

* Better error message when file can not be read

* Extend documentation with an example about docker compose secrets.

* Add all new env vars to variable lists

* Replace secret variables with file based secret variables in docker files

* Inform user about file based secrets passing

* Add myself to list of contributors

* Use token and also use file for password

* Handle newlines in secret

---------

Co-authored-by: Cromefire_ <[email protected]>
  • Loading branch information
britter and cromefire authored Oct 25, 2024
1 parent 4dfeaa3 commit 103005b
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 25 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
Contains work of the following people besides me:

* [Cromefire_](https://github.com/cromefire)
* [Benedikt Ritter](https://github.com/britter)
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ENV FRITZBOX_ENDPOINT_URL="http://fritz.box:49000" \
DYNDNS_SERVER_USERNAME="" \
DYNDNS_SERVER_PASSWORD="" \
CLOUDFLARE_API_EMAIL="" \
CLOUDFLARE_API_KEY="" \
CLOUDFLARE_API_KEY_FILE="" \
CLOUDFLARE_ZONES_IPV4="" \
CLOUDFLARE_ZONES_IPV6="" \
DEVICE_LOCAL_ADDRESS_IPV6=""
Expand Down
78 changes: 58 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,22 @@ You can use this strategy if you have:

In your `.env` file or your system environment variables you can be configured:

| Variable name | Description |
|------------------------|------------------------------------------------------|
| DYNDNS_SERVER_BIND | required, network interface to bind to, i.e. `:8080` |
| DYNDNS_SERVER_USERNAME | optional, username for the DynDNS service |
| DYNDNS_SERVER_PASSWORD | optional, password for the DynDNS service |
| Variable name | Description |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| DYNDNS_SERVER_BIND | required, network interface to bind to, i.e. `:8080`. |
| DYNDNS_SERVER_USERNAME | optional, username for the DynDNS service. |
| DYNDNS_SERVER_PASSWORD | optional, password for the DynDNS service. |
| DYNDNS_SERVER_PASSWORD_FILE | optional, path to a file containing the password for the DynDNS service. It's recommended to use this over `DYNDNS_SERVER_PASSWORD`. |

Now configure the FRITZ!Box router to push IP changes towards this service. Log into the admin panel and go to
`Internet > Shares > DynDNS tab` and setup a `Custom` provider:

| Property | Description / Value |
|------------|---------------------------------------------------------------------------------------|
| Update-URL | http://[server-ip]/ip?v4=\<ipaddr\>&v6=\<ip6addr\>&prefix=\<ip6lanprefix\> |
| Domain | Enter at least one domain name so the router can probe if the update was successfully |
| Username | Enter '_' if `DYNDNS_SERVER_USERNAME` env is unset |
| Password | Enter '_' if `DYNDNS_SERVER_PASSWORD` env is unset |
| Property | Description / Value |
|------------|----------------------------------------------------------------------------------------|
| Update-URL | http://[server-ip]/ip?v4=\<ipaddr\>&v6=\<ip6addr\>&prefix=\<ip6lanprefix\> |
| Domain | Enter at least one domain name so the router can probe if the update was successfully. |
| Username | Enter '_' if `DYNDNS_SERVER_USERNAME` is unset. |
| Password | Enter '_' if `DYNDNS_SERVER_PASSWORD` and `DYNDNS_SERVER_PASSWORD_FILE` are unset. |

If you specified credentials you need to append them as additional GET parameters into the Update-URL
like `&username=<username>&password=<pass>`.
Expand All @@ -73,7 +74,7 @@ In your `.env` file or your system environment variables you can be configured:
|----------------------------|--------------------------------------------------------------------------------------------------------|
| FRITZBOX_ENDPOINT_URL | optional, how can we reach the router, i.e. `http://fritz.box:49000`, the port should be 49000 anyway. |
| FRITZBOX_ENDPOINT_TIMEOUT | optional, a duration we give the router to respond, i.e. `10s`. |
| FRITZBOX_ENDPOINT_INTERVAL | optional, a duration how often we want to poll the WAN IPs from the router, i.e. `120s` |
| FRITZBOX_ENDPOINT_INTERVAL | optional, a duration how often we want to poll the WAN IPs from the router, i.e. `120s`. |

You can try the endpoint URL in the browser to make sure you have the correct port, you should receive
an `404 ERR_NOT_FOUND`.
Expand All @@ -90,13 +91,15 @@ to the config, you won't be able to see it again.

In your `.env` file or your system environment variables you can be configured:

| Variable name | Description |
|-----------------------|-------------------------------------------------------------------|
| CLOUDFLARE_API_TOKEN | required, your Cloudflare API Token |
| CLOUDFLARE_ZONES_IPV4 | comma-separated list of domains to update with new IPv4 addresses |
| CLOUDFLARE_ZONES_IPV6 | comma-separated list of domains to update with new IPv6 addresses |
| CLOUDFLARE_API_EMAIL | deprecated, your Cloudflare account email |
| CLOUDFLARE_API_KEY | deprecated, your Cloudflare Global API key |
| Variable name | Description |
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CLOUDFLARE_API_TOKEN | required if `CLOUDFLARE_API_TOKEN_FILE` is unset, your Cloudflare API Token. |
| CLOUDFLARE_API_TOKEN_FILE | required if `CLOUDFLARE_API_TOKEN` is unset, path to a file containing your Cloudflare API Token. It's recommended to use this over `CLOUDFLARE_API_TOKEN`. |
| CLOUDFLARE_ZONES_IPV4 | comma-separated list of domains to update with new IPv4 addresses. |
| CLOUDFLARE_ZONES_IPV6 | comma-separated list of domains to update with new IPv6 addresses. |
| CLOUDFLARE_API_EMAIL | deprecated, your Cloudflare account email. |
| CLOUDFLARE_API_KEY | deprecated, your Cloudflare Global API key. |
| CLOUDFLARE_API_KEY_FILE | deprecated, path to a file containing your Cloudflare Global API key. |

This service allows to update multiple records, an advanced example would be:

Expand Down Expand Up @@ -153,6 +156,41 @@ Now we could configure the FRITZ!Box
to `http://[docker-host-ip]:49000/ip?v4=<ipaddr>&v6=<ip6addr>&prefix=<ip6lanprefix>` and it should trigger the update
process.

## Passing secrets

As shown above, secrets can be passed via environment variables.
If passing secrets via environment variables does not work for your use case, it's also possible to pass them via the filesystem.
In order to pass a secret via a file, append `_FILE` to the respective environment variable name and configure it to point to the file containing the secret.
For example in order to pass the Cloudflare API token via a file, configure an environment variable with name `CLOUDFLARE_API_TOKEN_FILE` with the absolute path to a file containing the secret.

Here is an example `docker-compose.yml` passing the file `cloudflare_api_key.txt` from the host to the docker container using docker compose secrets:

```
version: '3.7'
services:
updater:
image: ghcr.io/cromefire/fritzbox-cloudflare-dyndns:1
network_mode: host
environment:
- DYNDNS_SERVER_BIND=:8080
- CLOUDFLARE_API_TOKEN_FILE=/run/secrets/cloudflare_api_token
- DYNDNS_SERVER_PASSWORD_FILE=/run/secrets/fb_server_password
- CLOUDFLARE_ZONES_IPV4=test.example.com
- CLOUDFLARE_ZONES_IPV6=test.example.com
secrets:
- cloudflare_api_token
- fb_server_password
secrets:
cloudflare_api_token:
file: ./cloudflare_api_token.txt
fb_server_password:
file: ./fb_server_password.txt
```

See https://docs.docker.com/compose/how-tos/use-secrets/ for more information about docker compose secrets.

## Docker build

A pre-built docker image is also available on this
Expand Down Expand Up @@ -188,4 +226,4 @@ trigger it by calling `http://127.0.0.1:8888/ip?v4=127.0.0.1&v6=::1` and review

## History & Credit

Most of the credit goes to [@adrianrudnik](https://github.com/adrianrudnik), who wrote and maintained the software for years. Meanwhile I stepped in at a later point when the repository was transferred to me to continue its basic maintenance should it be required.
Most of the credit goes to [@adrianrudnik](https://github.com/adrianrudnik), who wrote and maintained the software for years. Meanwhile I stepped in at a later point when the repository was transferred to me to continue its basic maintenance should it be required.
2 changes: 1 addition & 1 deletion alpine.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ENV FRITZBOX_ENDPOINT_URL="http://fritz.box:49000" \
DYNDNS_SERVER_USERNAME="" \
DYNDNS_SERVER_PASSWORD="" \
CLOUDFLARE_API_EMAIL="" \
CLOUDFLARE_API_KEY="" \
CLOUDFLARE_API_KEY_FILE="" \
CLOUDFLARE_ZONES_IPV4="" \
CLOUDFLARE_ZONES_IPV6="" \
DEVICE_LOCAL_ADDRESS_IPV6=""
Expand Down
26 changes: 23 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ func newFritzBox() *avm.FritzBox {
func newUpdater() *cloudflare.Updater {
u := cloudflare.NewUpdater(slog.Default())

token := os.Getenv("CLOUDFLARE_API_TOKEN")
token := readSecret("CLOUDFLARE_API_TOKEN")
email := os.Getenv("CLOUDFLARE_API_EMAIL")
key := os.Getenv("CLOUDFLARE_API_KEY")
key := readSecret("CLOUDFLARE_API_KEY")

if token == "" {
if email == "" || key == "" {
Expand Down Expand Up @@ -155,7 +155,7 @@ func startPushServer(out chan<- *net.IP, localIp *net.IP, cancel context.CancelC

server := dyndns.NewServer(out, localIp, slog.Default())
server.Username = os.Getenv("DYNDNS_SERVER_USERNAME")
server.Password = os.Getenv("DYNDNS_SERVER_PASSWORD")
server.Password = readSecret("DYNDNS_SERVER_PASSWORD")

s := &http.Server{
Addr: bind,
Expand Down Expand Up @@ -272,3 +272,23 @@ func startPollServer(out chan<- *net.IP, localIp *net.IP) {
}
}()
}

func readSecret(envName string) string {
secret := os.Getenv(envName)

if secret != "" {
slog.Info("Secret passed via environment variable " + envName + ". It's recommended to pass secrets via files, see https://github.com/cromefire/fritzbox-cloudflare-dyndns?tab=readme-ov-file#passing-secrets.")
return secret
}

passwordFilePath := os.Getenv(envName + "_FILE")
if passwordFilePath != "" {
content, err := os.ReadFile(passwordFilePath)
if err != nil {
slog.Error("Failed to read secret from file "+passwordFilePath, logging.ErrorAttr(err))
} else {
secret = strings.TrimSuffix(strings.TrimSuffix(string(content), "\r\n"), "\n")
}
}
return secret
}

0 comments on commit 103005b

Please sign in to comment.