diff --git a/Dockerfile b/Dockerfile index d49cf6d3..abe308db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,6 +51,8 @@ ENV VPNSP=pia \ # PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only OPENVPN_USER= \ OPENVPN_PASSWORD= \ + USER_SECRETFILE=/run/secrets/openvpn_user \ + PASSWORD_SECRETFILE=/run/secrets/openvpn_password \ REGION= \ # PIA only PIA_ENCRYPTION=strong \ @@ -69,6 +71,8 @@ ENV VPNSP=pia \ PORT= \ # Cyberghost only CYBERGHOST_GROUP="Premium UDP Europe" \ + OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \ + OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \ # NordVPN only SERVER_NUMBER= \ # Openvpn @@ -102,11 +106,14 @@ ENV VPNSP=pia \ HTTPPROXY_PORT=8888 \ HTTPPROXY_USER= \ HTTPPROXY_PASSWORD= \ + HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \ + HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \ # Shadowsocks SHADOWSOCKS=off \ SHADOWSOCKS_LOG=off \ SHADOWSOCKS_PORT=8388 \ SHADOWSOCKS_PASSWORD= \ + SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \ SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \ UPDATER_PERIOD=0 ENTRYPOINT ["/entrypoint"] diff --git a/README.md b/README.md index 87242300..cdbf9111 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado ```bash docker run -d --name gluetun --cap-add=NET_ADMIN \ -e VPNSP="private internet access" -e REGION="CA Montreal" \ - -e OPENVPN_USER=js89ds7 -e PASSWORD=8fd9s239G \ + -e OPENVPN_USER=js89ds7 -e OPENVPN_PASSWORD=8fd9s239G \ -v /yourpath:/gluetun \ qmcgaw/private-internet-access ``` @@ -62,6 +62,8 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with: ```bash + echo "your openvpn username" > openvpn_user + echo "your openvpn password" > openvpn_password docker-compose up -d ``` @@ -71,6 +73,7 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado - Use `-p 8888:8888/tcp` to access the HTTP web proxy - Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the Shadowsocks proxy - Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in + - Use [Docker secrets](#Docker-secrets) to read your credentials instead of environment variables **If you encounter an issue with the tun device not being available, see [the FAQ](https://github.com/qdm12/gluetun/blob/master/doc/faq.md#how-to-fix-openvpn-failing-to-start)** @@ -163,7 +166,9 @@ docker run --rm --network=container:gluetun alpine:3.12 wget -qO- https://ipinfo | `REGION` | | One of the Cyberghost regions, [Wiki page](https://github.com/qdm12/gluetun/wiki/Cyberghost-Servers) | VPN server country | | `CYBERGHOST_GROUP` | `Premium UDP Europe` | One of the server groups (see above Wiki page) | Server group | - **Additional setup steps**: Bind mount your `client.key` file to `/gluetun/client.key` and your `client.crt` file to `/gluetun/client.crt`. For example, you can use with your `docker run` command: + **Additional setup steps**: If you use docker Swarm or docker-compose, you should use the [Docker secrets](#Docker-secrets) `openvpn_clientkey` and `openvpn_clientcrt`. + + Otherwise, bind mount your `client.key` and `client.crt` files with, for example: ```sh -v /yourpath/client.key:/gluetun/client.key:ro -v /yourpath/client.crt:/gluetun/client.crt:ro @@ -282,6 +287,25 @@ None of the following values are required. | `VERSION_INFORMATION` | `on` | `on`, `off` | Logs a message indicating if a newer version is available once the VPN is connected | | `UPDATER_PERIOD` | `0` | Valid duration string such as `24h` | Period to update all VPN servers information in memory and to /gluetun/servers.json. Set to `0` to disable. This does a burst of DNS over TLS requests, which may be blocked if you set `BLOCK_MALICIOUS=on` for example. | +## Docker secrets + +If you use Docker Compose or Docker Swarm, you can optionally use [Docker secret files](https://docs.docker.com/engine/swarm/secrets/) for all sensitive values such as your Openvpn credentials, instead of using environment variables. + +The following secrets can be used: + +- `openvpn_user` +- `openvpn_password` +- `openvpn_clientkey` +- `openvpn_clientcrt` +- `httpproxy_username` +- `httpproxy_password` +- `shadowsocks_password` + +By default, `openvpn_user` and `openvpn_password` are set in [docker-compose.yml](docker-compose.yml) + +Note that you can change the secret file path in the container by changing the environment variable in the form `_SECRETFILE`. +For example, `OPENVPN_PASSWORD_SECRETFILE` defaults to `/run/secrets/openvpn_password` which you can change. + ## Connect to it There are various ways to achieve this, depending on your use case. diff --git a/docker-compose.yml b/docker-compose.yml index 3c729048..75387824 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,25 +14,18 @@ services: # command: volumes: - /yourpath:/gluetun + secrets: + - openvpn_user + - openvpn_password environment: # More variables are available, see the readme table - VPNSP=private internet access - # Timezone for accurate logs times - TZ= - - # All VPN providers - - OPENVPN_USER=js89ds7 - - # All VPN providers but Mullvad - - OPENVPN_PASSWORD=8fd9s239G - - # Cyberghost only - - CLIENT_KEY= - - # All VPN providers but Mullvad - - REGION=Austria - - # Mullvad only - - COUNTRY=Sweden restart: always + +secrets: + openvpn_user: + file: ./openvpn_user + openvpn_password: + file: ./openvpn_password diff --git a/internal/params/cyberghost.go b/internal/params/cyberghost.go index f83990f5..80430ff0 100644 --- a/internal/params/cyberghost.go +++ b/internal/params/cyberghost.go @@ -3,8 +3,6 @@ package params import ( "encoding/pem" "fmt" - "io/ioutil" - "os" "strings" "github.com/qdm12/gluetun/internal/constants" @@ -25,23 +23,15 @@ func (p *reader) GetCyberghostRegions() (regions []string, err error) { return p.envParams.GetCSVInPossibilities("REGION", constants.CyberghostRegionChoices()) } -// GetCyberghostClientKey obtains the one line client key to use for openvpn from the -// file at /gluetun/client.key. +// GetCyberghostClientKey obtains the client key to use for openvpn +// from the secret file /run/secrets/openvpn_clientkey or from the file +// /gluetun/client.key. func (p *reader) GetCyberghostClientKey() (clientKey string, err error) { - const filepath = string(constants.ClientKey) - file, err := p.os.OpenFile(filepath, os.O_RDONLY, 0) + b, err := p.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", string(constants.ClientKey)) if err != nil { return "", err } - content, err := ioutil.ReadAll(file) - if err != nil { - _ = file.Close() - return "", err - } - if err := file.Close(); err != nil { - return "", err - } - return extractClientKey(content) + return extractClientKey(b) } func extractClientKey(b []byte) (key string, err error) { @@ -57,23 +47,15 @@ func extractClientKey(b []byte) (key string, err error) { return s, nil } -// GetCyberghostClientCertificate obtains the client certificate to use for openvpn from the -// file at /gluetun/client.crt. +// GetCyberghostClientCertificate obtains the client certificate to use for openvpn +// from the secret file /run/secrets/openvpn_clientcrt or from the file +// /gluetun/client.crt. func (p *reader) GetCyberghostClientCertificate() (clientCertificate string, err error) { - const filepath = string(constants.ClientCertificate) - file, err := p.os.OpenFile(filepath, os.O_RDONLY, 0) + b, err := p.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", string(constants.ClientCertificate)) if err != nil { return "", err } - content, err := ioutil.ReadAll(file) - if err != nil { - _ = file.Close() - return "", err - } - if err := file.Close(); err != nil { - return "", err - } - return extractClientCertificate(content) + return extractClientCertificate(b) } func extractClientCertificate(b []byte) (certificate string, err error) { diff --git a/internal/params/httpproxy.go b/internal/params/httpproxy.go index 0b2b6740..c1c7823f 100644 --- a/internal/params/httpproxy.go +++ b/internal/params/httpproxy.go @@ -49,26 +49,28 @@ func (r *reader) GetHTTPProxyPort() (port uint16, err error) { return r.envParams.GetPort("HTTPPROXY_PORT", retroKeysOption, libparams.Default("8888")) } -// GetHTTPProxyUser obtains the HTTP proxy server user from the environment variable -// HTTPPROXY_USER, and using TINYPROXY_USER and PROXY_USER as retro-compatibility names. +// GetHTTPProxyUser obtains the HTTP proxy server user. +// It first tries to use the HTTPPROXY_USER environment variable (easier for the end user) +// and then tries to read from the secret file httpproxy_user if nothing was found. func (r *reader) GetHTTPProxyUser() (user string, err error) { - retroKeysOption := libparams.RetroKeys( + const compulsory = false + return r.getFromEnvOrSecretFile( + "HTTPPROXY_USER", + compulsory, []string{"TINYPROXY_USER", "PROXY_USER"}, - r.onRetroActive, ) - return r.envParams.GetEnv("HTTPPROXY_USER", - retroKeysOption, libparams.CaseSensitiveValue(), libparams.Unset()) } -// GetHTTPProxyPassword obtains the HTTP proxy server password from the environment variable -// HTTPPROXY_PASSWORD, and using TINYPROXY_PASSWORD and PROXY_PASSWORD as retro-compatibility names. +// GetHTTPProxyPassword obtains the HTTP proxy server password. +// It first tries to use the HTTPPROXY_PASSWORD environment variable (easier for the end user) +// and then tries to read from the secret file httpproxy_password if nothing was found. func (r *reader) GetHTTPProxyPassword() (password string, err error) { - retroKeysOption := libparams.RetroKeys( + const compulsory = false + return r.getFromEnvOrSecretFile( + "HTTPPROXY_USER", + compulsory, []string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"}, - r.onRetroActive, ) - return r.envParams.GetEnv("HTTPPROXY_PASSWORD", - retroKeysOption, libparams.CaseSensitiveValue(), libparams.Unset()) } // GetHTTPProxyStealth obtains the HTTP proxy server stealth mode diff --git a/internal/params/openvpn.go b/internal/params/openvpn.go index 36a6bab4..2718ec3d 100644 --- a/internal/params/openvpn.go +++ b/internal/params/openvpn.go @@ -9,23 +9,19 @@ import ( ) // GetUser obtains the user to use to connect to the VPN servers. +// It first tries to use the OPENVPN_USER environment variable (easier for the end user) +// and then tries to read from the secret file openvpn_user if nothing was found. func (r *reader) GetUser() (user string, err error) { - return r.envParams.GetEnv("OPENVPN_USER", - libparams.CaseSensitiveValue(), - libparams.Compulsory(), - libparams.RetroKeys([]string{"USER"}, r.onRetroActive), - libparams.Unset()) + const compulsory = true + return r.getFromEnvOrSecretFile("OPENVPN_USER", compulsory, []string{"USER"}) } // GetPassword obtains the password to use to connect to the VPN servers. +// It first tries to use the OPENVPN_PASSWORD environment variable (easier for the end user) +// and then tries to read from the secret file openvpn_password if nothing was found. func (r *reader) GetPassword() (s string, err error) { - options := []libparams.GetEnvSetter{ - libparams.CaseSensitiveValue(), - libparams.Unset(), - libparams.Compulsory(), - libparams.RetroKeys([]string{"PASSWORD"}, r.onRetroActive), - } - return r.envParams.GetEnv("OPENVPN_PASSWORD", options...) + const compulsory = true + return r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", compulsory, []string{"PASSWORD"}) } // GetNetworkProtocol obtains the network protocol to use to connect to the diff --git a/internal/params/secrets.go b/internal/params/secrets.go new file mode 100644 index 00000000..5f1df179 --- /dev/null +++ b/internal/params/secrets.go @@ -0,0 +1,108 @@ +package params + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/qdm12/gluetun/internal/os" + libparams "github.com/qdm12/golibs/params" +) + +var ( + ErrGetSecretFilepath = errors.New("cannot get secret file path from env") + ErrReadSecretFile = errors.New("cannot read secret file") + ErrSecretFileIsEmpty = errors.New("secret file is empty") + ErrReadNonSecretFile = errors.New("cannot read non secret file") + ErrFilesDoNotExist = errors.New("files do not exist") +) + +func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKeys []string) (value string, err error) { + envOptions := []libparams.GetEnvSetter{ + libparams.Compulsory(), // to fallback on file reading + libparams.CaseSensitiveValue(), + libparams.Unset(), + libparams.RetroKeys(retroKeys, r.onRetroActive), + } + value, envErr := r.envParams.GetEnv(envKey, envOptions...) + if envErr == nil { + return value, nil + } + + defaultSecretFile := "/run/secrets/" + strings.ToLower(envKey) + filepath, err := r.envParams.GetEnv(envKey+"_SECRETFILE", + libparams.CaseSensitiveValue(), + libparams.Default(defaultSecretFile), + ) + if err != nil { + return "", fmt.Errorf("%w: %s", ErrGetSecretFilepath, err) + } + + file, fileErr := r.os.OpenFile(filepath, os.O_RDONLY, 0) + if os.IsNotExist(fileErr) { + if compulsory { + return "", envErr + } + return "", nil + } else if fileErr != nil { + return "", fmt.Errorf("%w: %s", ErrReadSecretFile, fileErr) + } + + b, err := ioutil.ReadAll(file) + if err != nil { + return "", fmt.Errorf("%w: %s", ErrReadSecretFile, err) + } + + value = string(b) + if compulsory && len(value) == 0 { + return "", ErrSecretFileIsEmpty + } + + return value, nil +} + +// Tries to read from the secret file then the non secret file. +func (r *reader) getFromFileOrSecretFile(secretName, filepath string) ( + b []byte, err error) { + defaultSecretFile := "/run/secrets/" + strings.ToLower(secretName) + secretFilepath, err := r.envParams.GetEnv(strings.ToUpper(secretName)+"_SECRETFILE", + libparams.CaseSensitiveValue(), + libparams.Default(defaultSecretFile), + ) + if err != nil { + return b, fmt.Errorf("%w: %s", ErrGetSecretFilepath, err) + } + + b, err = readFromFile(r.os.OpenFile, secretFilepath) + if err != nil && !os.IsNotExist(err) { + return b, fmt.Errorf("%w: %s", ErrReadSecretFile, err) + } else if err == nil { + return b, nil + } + + // Secret file does not exist, try the non secret file + b, err = readFromFile(r.os.OpenFile, filepath) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("%w: %s", ErrReadSecretFile, err) + } else if err == nil { + return b, nil + } + return nil, fmt.Errorf("%w: %s and %s", ErrFilesDoNotExist, secretFilepath, filepath) +} + +func readFromFile(openFile os.OpenFileFunc, filepath string) (b []byte, err error) { + file, err := openFile(filepath, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + b, err = ioutil.ReadAll(file) + if err != nil { + _ = file.Close() + return nil, err + } + if err := file.Close(); err != nil { + return nil, err + } + return b, nil +} diff --git a/internal/params/shadowsocks.go b/internal/params/shadowsocks.go index 2f97d212..9590653c 100644 --- a/internal/params/shadowsocks.go +++ b/internal/params/shadowsocks.go @@ -32,10 +32,12 @@ func (r *reader) GetShadowSocksPort() (port uint16, err error) { return uint16(portUint64), err } -// GetShadowSocksPassword obtains the ShadowSocks server password from the environment variable -// SHADOWSOCKS_PASSWORD. +// GetShadowSocksPassword obtains the ShadowSocks server password. +// It first tries to use the SHADOWSOCKS_PASSWORD environment variable (easier for the end user) +// and then tries to read from the secret file shadowsocks_password if nothing was found. func (r *reader) GetShadowSocksPassword() (password string, err error) { - return r.envParams.GetEnv("SHADOWSOCKS_PASSWORD", libparams.CaseSensitiveValue(), libparams.Unset()) + const compulsory = false + return r.getFromEnvOrSecretFile("SHADOWSOCKS_PASSWORD", compulsory, nil) } // GetShadowSocksMethod obtains the ShadowSocks method to use from the environment variable