From 486ae93a2afa637799e0c43d732a162a502a2fac Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 4 Oct 2024 15:41:50 +0200 Subject: [PATCH 01/10] Add possibility to pass secrets via files Resolves #30 --- README.md | 8 +++++++- main.go | 25 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3deb949..f1eeb1e 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,12 @@ like `::1234:5678:90ab:cdef` to `::1:1234:5678:90ab:cdef` |---------------------------|-------------------------------------------------| | DEVICE_LOCAL_ADDRESS_IPV6 | required, enter the local part of the device IP | +## Secrets + +Each secret can be passed either as an environment variable directly, or via a file. +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. + ## Docker compose setup Here is an example `docker-compose.yml` with all features activated: @@ -188,4 +194,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. \ No newline at end of file +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. diff --git a/main.go b/main.go index b6082fb..6fd12e1 100644 --- a/main.go +++ b/main.go @@ -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 == "" { @@ -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, @@ -272,3 +272,22 @@ func startPollServer(out chan<- *net.IP, localIp *net.IP) { } }() } + +func readSecret(envName string) string { + password := os.Getenv(envName) + + if password != "" { + return password + } + + passwordFilePath := os.Getenv(envName + "_FILE") + if passwordFilePath != "" { + content, err := os.ReadFile(passwordFilePath) + if err != nil { + slog.Error("Failed to read DynDns server password from file", logging.ErrorAttr(err)) + } else { + password = string(content) + } + } + return password +} From 8de4d4bd21a3d90d5205229825b69bfc4aaae596 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 4 Oct 2024 19:56:38 +0200 Subject: [PATCH 02/10] Stick to 'secret' terminology --- main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 6fd12e1..844b40a 100644 --- a/main.go +++ b/main.go @@ -274,10 +274,10 @@ func startPollServer(out chan<- *net.IP, localIp *net.IP) { } func readSecret(envName string) string { - password := os.Getenv(envName) + secret := os.Getenv(envName) - if password != "" { - return password + if secret != "" { + return secret } passwordFilePath := os.Getenv(envName + "_FILE") @@ -286,8 +286,8 @@ func readSecret(envName string) string { if err != nil { slog.Error("Failed to read DynDns server password from file", logging.ErrorAttr(err)) } else { - password = string(content) + secret = string(content) } } - return password + return secret } From 2ebb1fa51849d7b79aba693905d923e6dcf2dd57 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 4 Oct 2024 20:00:50 +0200 Subject: [PATCH 03/10] Better error message when file can not be read --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 844b40a..a5ff1cc 100644 --- a/main.go +++ b/main.go @@ -284,7 +284,7 @@ func readSecret(envName string) string { if passwordFilePath != "" { content, err := os.ReadFile(passwordFilePath) if err != nil { - slog.Error("Failed to read DynDns server password from file", logging.ErrorAttr(err)) + slog.Error("Failed to read secret from file "+passwordFilePath, logging.ErrorAttr(err)) } else { secret = string(content) } From 39fd4e4244501398286b6194f9f3eb0fcc6a81e8 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Thu, 24 Oct 2024 11:15:47 +0200 Subject: [PATCH 04/10] Extend documentation with an example about docker compose secrets. --- README.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f1eeb1e..1a13834 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,6 @@ like `::1234:5678:90ab:cdef` to `::1:1234:5678:90ab:cdef` |---------------------------|-------------------------------------------------| | DEVICE_LOCAL_ADDRESS_IPV6 | required, enter the local part of the device IP | -## Secrets - -Each secret can be passed either as an environment variable directly, or via a file. -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. - ## Docker compose setup Here is an example `docker-compose.yml` with all features activated: @@ -159,6 +153,37 @@ Now we could configure the FRITZ!Box to `http://[docker-host-ip]:49000/ip?v4=&v6=&prefix=` 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_KEY_FILE=/run/secrets/cloudflare_api_key + - CLOUDFLARE_ZONES_IPV4=test.example.com + - CLOUDFLARE_ZONES_IPV6=test.example.com + secrets: + - cloudflare_api_key + +secrets: + cloudflare_api_key: + file: ./cloudflare_api_key.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 From 6afcfbfbea104b0573500385ff7cbbfac6c040a9 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 25 Oct 2024 11:19:38 +0200 Subject: [PATCH 05/10] Add all new env vars to variable lists --- README.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1a13834..fe3503f 100644 --- a/README.md +++ b/README.md @@ -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=\&v6=\&prefix=\ | -| 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=\&v6=\&prefix=\ | +| 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=&password=`. @@ -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`. @@ -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: From 39a8cb650def5c64a0743bd5d61bdf348521dbf7 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 25 Oct 2024 11:20:43 +0200 Subject: [PATCH 06/10] Replace secret variables with file based secret variables in docker files --- Dockerfile | 2 +- alpine.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 44a33c4..a56d068 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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="" diff --git a/alpine.Dockerfile b/alpine.Dockerfile index f3e0114..5811822 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -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="" From f203933f4fea096e3857f19c4e2b949a590b54e8 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 25 Oct 2024 11:40:01 +0200 Subject: [PATCH 07/10] Inform user about file based secrets passing --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index a5ff1cc..e8e1427 100644 --- a/main.go +++ b/main.go @@ -277,6 +277,7 @@ 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 } From 0780f74d77266448d2e35a148c597d02663eb3d0 Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 25 Oct 2024 11:40:45 +0200 Subject: [PATCH 08/10] Add myself to list of contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4d3e477..6be908b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,3 +3,4 @@ Contains work of the following people besides me: * [Cromefire_](https://github.com/cromefire) +* [Benedikt Ritter](https://github.com/britter) From 523136c87efc07b9eb1edc4181ddaf5b636adc75 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Fri, 25 Oct 2024 12:07:02 +0200 Subject: [PATCH 09/10] Use token and also use file for password --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fe3503f..35f6085 100644 --- a/README.md +++ b/README.md @@ -174,15 +174,19 @@ services: network_mode: host environment: - DYNDNS_SERVER_BIND=:8080 - - CLOUDFLARE_API_KEY_FILE=/run/secrets/cloudflare_api_key + - 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_key + - cloudflare_api_token + - fb_server_password secrets: - cloudflare_api_key: - file: ./cloudflare_api_key.txt + 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. From 46c76507d81509165e7c6117f28816461bb29c48 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Fri, 25 Oct 2024 12:07:28 +0200 Subject: [PATCH 10/10] Handle newlines in secret --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index e8e1427..3ea34e1 100644 --- a/main.go +++ b/main.go @@ -287,7 +287,7 @@ func readSecret(envName string) string { if err != nil { slog.Error("Failed to read secret from file "+passwordFilePath, logging.ErrorAttr(err)) } else { - secret = string(content) + secret = strings.TrimSuffix(strings.TrimSuffix(string(content), "\r\n"), "\n") } } return secret