Feature: Docker secrets, refers to #306

This commit is contained in:
Quentin McGaw
2020-12-29 20:47:56 +00:00
parent 258e150ebf
commit 5917bb10e4
8 changed files with 187 additions and 73 deletions

View File

@@ -51,6 +51,8 @@ ENV VPNSP=pia \
# PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only # PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only
OPENVPN_USER= \ OPENVPN_USER= \
OPENVPN_PASSWORD= \ OPENVPN_PASSWORD= \
USER_SECRETFILE=/run/secrets/openvpn_user \
PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
REGION= \ REGION= \
# PIA only # PIA only
PIA_ENCRYPTION=strong \ PIA_ENCRYPTION=strong \
@@ -69,6 +71,8 @@ ENV VPNSP=pia \
PORT= \ PORT= \
# Cyberghost only # Cyberghost only
CYBERGHOST_GROUP="Premium UDP Europe" \ CYBERGHOST_GROUP="Premium UDP Europe" \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# NordVPN only # NordVPN only
SERVER_NUMBER= \ SERVER_NUMBER= \
# Openvpn # Openvpn
@@ -102,11 +106,14 @@ ENV VPNSP=pia \
HTTPPROXY_PORT=8888 \ HTTPPROXY_PORT=8888 \
HTTPPROXY_USER= \ HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \ HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \
# Shadowsocks # Shadowsocks
SHADOWSOCKS=off \ SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \ SHADOWSOCKS_LOG=off \
SHADOWSOCKS_PORT=8388 \ SHADOWSOCKS_PORT=8388 \
SHADOWSOCKS_PASSWORD= \ SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \ SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \
UPDATER_PERIOD=0 UPDATER_PERIOD=0
ENTRYPOINT ["/entrypoint"] ENTRYPOINT ["/entrypoint"]

View File

@@ -54,7 +54,7 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado
```bash ```bash
docker run -d --name gluetun --cap-add=NET_ADMIN \ docker run -d --name gluetun --cap-add=NET_ADMIN \
-e VPNSP="private internet access" -e REGION="CA Montreal" \ -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 \ -v /yourpath:/gluetun \
qmcgaw/private-internet-access 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: or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
```bash ```bash
echo "your openvpn username" > openvpn_user
echo "your openvpn password" > openvpn_password
docker-compose up -d 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 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 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 `-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)** **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 | | `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 | | `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 ```sh
-v /yourpath/client.key:/gluetun/client.key:ro -v /yourpath/client.crt:/gluetun/client.crt:ro -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 | | `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. | | `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 `<capitalizedSecretName>_SECRETFILE`.
For example, `OPENVPN_PASSWORD_SECRETFILE` defaults to `/run/secrets/openvpn_password` which you can change.
## Connect to it ## Connect to it
There are various ways to achieve this, depending on your use case. There are various ways to achieve this, depending on your use case.

View File

@@ -14,25 +14,18 @@ services:
# command: # command:
volumes: volumes:
- /yourpath:/gluetun - /yourpath:/gluetun
secrets:
- openvpn_user
- openvpn_password
environment: environment:
# More variables are available, see the readme table # More variables are available, see the readme table
- VPNSP=private internet access - VPNSP=private internet access
# Timezone for accurate logs times # Timezone for accurate logs times
- TZ= - 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 restart: always
secrets:
openvpn_user:
file: ./openvpn_user
openvpn_password:
file: ./openvpn_password

View File

@@ -3,8 +3,6 @@ package params
import ( import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io/ioutil"
"os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/constants" "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()) return p.envParams.GetCSVInPossibilities("REGION", constants.CyberghostRegionChoices())
} }
// GetCyberghostClientKey obtains the one line client key to use for openvpn from the // GetCyberghostClientKey obtains the client key to use for openvpn
// file at /gluetun/client.key. // from the secret file /run/secrets/openvpn_clientkey or from the file
// /gluetun/client.key.
func (p *reader) GetCyberghostClientKey() (clientKey string, err error) { func (p *reader) GetCyberghostClientKey() (clientKey string, err error) {
const filepath = string(constants.ClientKey) b, err := p.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", string(constants.ClientKey))
file, err := p.os.OpenFile(filepath, os.O_RDONLY, 0)
if err != nil { if err != nil {
return "", err return "", err
} }
content, err := ioutil.ReadAll(file) return extractClientKey(b)
if err != nil {
_ = file.Close()
return "", err
}
if err := file.Close(); err != nil {
return "", err
}
return extractClientKey(content)
} }
func extractClientKey(b []byte) (key string, err error) { func extractClientKey(b []byte) (key string, err error) {
@@ -57,23 +47,15 @@ func extractClientKey(b []byte) (key string, err error) {
return s, nil return s, nil
} }
// GetCyberghostClientCertificate obtains the client certificate to use for openvpn from the // GetCyberghostClientCertificate obtains the client certificate to use for openvpn
// file at /gluetun/client.crt. // from the secret file /run/secrets/openvpn_clientcrt or from the file
// /gluetun/client.crt.
func (p *reader) GetCyberghostClientCertificate() (clientCertificate string, err error) { func (p *reader) GetCyberghostClientCertificate() (clientCertificate string, err error) {
const filepath = string(constants.ClientCertificate) b, err := p.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", string(constants.ClientCertificate))
file, err := p.os.OpenFile(filepath, os.O_RDONLY, 0)
if err != nil { if err != nil {
return "", err return "", err
} }
content, err := ioutil.ReadAll(file) return extractClientCertificate(b)
if err != nil {
_ = file.Close()
return "", err
}
if err := file.Close(); err != nil {
return "", err
}
return extractClientCertificate(content)
} }
func extractClientCertificate(b []byte) (certificate string, err error) { func extractClientCertificate(b []byte) (certificate string, err error) {

View File

@@ -49,26 +49,28 @@ func (r *reader) GetHTTPProxyPort() (port uint16, err error) {
return r.envParams.GetPort("HTTPPROXY_PORT", retroKeysOption, libparams.Default("8888")) return r.envParams.GetPort("HTTPPROXY_PORT", retroKeysOption, libparams.Default("8888"))
} }
// GetHTTPProxyUser obtains the HTTP proxy server user from the environment variable // GetHTTPProxyUser obtains the HTTP proxy server user.
// HTTPPROXY_USER, and using TINYPROXY_USER and PROXY_USER as retro-compatibility names. // 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) { 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"}, []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 // GetHTTPProxyPassword obtains the HTTP proxy server password.
// HTTPPROXY_PASSWORD, and using TINYPROXY_PASSWORD and PROXY_PASSWORD as retro-compatibility names. // 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) { 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"}, []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 // GetHTTPProxyStealth obtains the HTTP proxy server stealth mode

View File

@@ -9,23 +9,19 @@ import (
) )
// GetUser obtains the user to use to connect to the VPN servers. // 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) { func (r *reader) GetUser() (user string, err error) {
return r.envParams.GetEnv("OPENVPN_USER", const compulsory = true
libparams.CaseSensitiveValue(), return r.getFromEnvOrSecretFile("OPENVPN_USER", compulsory, []string{"USER"})
libparams.Compulsory(),
libparams.RetroKeys([]string{"USER"}, r.onRetroActive),
libparams.Unset())
} }
// GetPassword obtains the password to use to connect to the VPN servers. // 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) { func (r *reader) GetPassword() (s string, err error) {
options := []libparams.GetEnvSetter{ const compulsory = true
libparams.CaseSensitiveValue(), return r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", compulsory, []string{"PASSWORD"})
libparams.Unset(),
libparams.Compulsory(),
libparams.RetroKeys([]string{"PASSWORD"}, r.onRetroActive),
}
return r.envParams.GetEnv("OPENVPN_PASSWORD", options...)
} }
// GetNetworkProtocol obtains the network protocol to use to connect to the // GetNetworkProtocol obtains the network protocol to use to connect to the

108
internal/params/secrets.go Normal file
View File

@@ -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
}

View File

@@ -32,10 +32,12 @@ func (r *reader) GetShadowSocksPort() (port uint16, err error) {
return uint16(portUint64), err return uint16(portUint64), err
} }
// GetShadowSocksPassword obtains the ShadowSocks server password from the environment variable // GetShadowSocksPassword obtains the ShadowSocks server password.
// SHADOWSOCKS_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) { 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 // GetShadowSocksMethod obtains the ShadowSocks method to use from the environment variable