From 103005bb12c0018229d45f1edce07ffed757a9fb Mon Sep 17 00:00:00 2001 From: Benedikt Ritter Date: Fri, 25 Oct 2024 12:13:11 +0200 Subject: [PATCH] Add possibility to pass secrets via files (#31) * 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_ --- CONTRIBUTORS.md | 1 + Dockerfile | 2 +- README.md | 78 +++++++++++++++++++++++++++++++++++------------ alpine.Dockerfile | 2 +- main.go | 26 ++++++++++++++-- 5 files changed, 84 insertions(+), 25 deletions(-) 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) 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/README.md b/README.md index 3deb949..35f6085 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: @@ -153,6 +156,41 @@ 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_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 @@ -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. \ 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/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="" diff --git a/main.go b/main.go index b6082fb..3ea34e1 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,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 +}