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

Add possibility to pass secrets via files #31

Merged
merged 10 commits into from
Oct 25, 2024
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 @@ -13,13 +13,13 @@
# Build deployable server
FROM gcr.io/distroless/static:debug

ENV FRITZBOX_ENDPOINT_URL="http://fritz.box:49000" \

Check warning on line 16 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/arm64, arm64, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DYNDNS_SERVER_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 16 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/arm64, arm64, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "CLOUDFLARE_API_KEY_FILE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 16 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/amd64, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DYNDNS_SERVER_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 16 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/amd64, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "CLOUDFLARE_API_KEY_FILE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 16 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/arm/v7, arm, arm)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DYNDNS_SERVER_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 16 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/arm/v7, arm, arm)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "CLOUDFLARE_API_KEY_FILE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
FRITZBOX_ENDPOINT_TIMEOUT="30s" \
DYNDNS_SERVER_BIND=":8080" \
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 @@ -13,13 +13,13 @@
# Build deployable server
FROM alpine:3

ENV FRITZBOX_ENDPOINT_URL="http://fritz.box:49000" \

Check warning on line 16 in alpine.Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/arm/v6, arm, arm, alpine.Dockerfile)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DYNDNS_SERVER_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 16 in alpine.Dockerfile

View workflow job for this annotation

GitHub Actions / build (linux/arm/v6, arm, arm, alpine.Dockerfile)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "CLOUDFLARE_API_KEY_FILE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
FRITZBOX_ENDPOINT_TIMEOUT="30s" \
DYNDNS_SERVER_BIND=":8080" \
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
}
Loading