diff --git a/Dockerfile b/Dockerfile index b0dec7a..8f7b16e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM --platform=$BUILDPLATFORM golang:1.22.5-alpine3.20 AS build +FROM --platform=$BUILDPLATFORM golang:1.22.6-alpine3.20 AS build WORKDIR /application COPY . ./ ARG TARGETOS diff --git a/README.md b/README.md index 25a912b..5a8fcf5 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,20 @@ You must set up regular expressions for each HTTP method the client application The name of a parameter should be "-allow", followed by the HTTP method name (for example, `-allowGET`). The request will be allowed if that parameter is set and the incoming request matches the method and path matching the regexp. If it is not set, then the corresponding HTTP method will not be allowed. +It is also possible to configure the allowlist via environment variables. The variables are called "SP_ALLOW_", followed by the HTTP method (for example, `SP_ALLLOW_GET`). + +If both commandline parameter and environment variable is configured for a particular HTTP method, the environment variable is ignored. + Use Go's regexp syntax to create the patterns for these parameters. To avoid insecure configurations, the characters ^ at the beginning and $ at the end of the string are automatically added. Note: invalid regexp results in program termination. -Examples: +Examples (command line): + `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2. + `'-allowHEAD=.*` allows all HEAD requests. +Examples (env variables): ++ `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2. ++ `'SP_ALLOW_HEAD=".*"` allows all HEAD requests. + For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/). An excellent online regexp tester is [regex101.com](https://regex101.com/). @@ -70,7 +78,7 @@ To determine which HTTP requests your client application uses, you could switch ### Container health check -Health checks are disabled by default. As the socket-proxy container may not be exposed to a public network, a separate health check binary is included in the container image. To activate the health check, the `-allowhealthcheck` parameter must be set. Then, a health check is possible for example with the following docker-compose snippet: +Health checks are disabled by default. As the socket-proxy container may not be exposed to a public network, a separate health check binary is included in the container image. To activate the health check, the `-allowhealthcheck` parameter or the environment variable `SP_ALLOWHEALTHCHECK=true` must be set. Then, a health check is possible for example with the following docker-compose snippet: ``` compose.yaml # [...] @@ -83,7 +91,7 @@ Health checks are disabled by default. As the socket-proxy container may not be ``` ### Socket watchdog -In certain circumstances (for example, after a Docker engine update), the socket connection may break, causing the client application to fail. To prevent this, the socket-proxy can be configured to check the socket availability at regular intervals. If the socket is not available, the socket-proxy will be stopped so the container orchestrator can restart it. This feature is disabled by default. To enable it, set the `-watchdoginterval` parameter to the desired interval in seconds and set the `-stoponwatchdog` parameter. If `-stoponwatchdog`is not set, the watchdog will only log an error message and continue to run (the problem would still exist in that case). +In certain circumstances (for example, after a Docker engine update), the socket connection may break, causing the client application to fail. To prevent this, the socket-proxy can be configured to check the socket availability at regular intervals. If the socket is not available, the socket-proxy will be stopped so the container orchestrator can restart it. This feature is disabled by default. To enable it, set the `-watchdoginterval` parameter (or `SP_WATCHDOGINTERVAL` env variable) to the desired interval in seconds and set the `-stoponwatchdog` parameter (or `SP_STOPONWATCHDOG=true`). If `-stoponwatchdog`is not set, the watchdog will only log an error message and continue to run (the problem would still exist in that case). ### Example for proxying the docker socket to Traefik @@ -150,20 +158,22 @@ To log the API calls of the client application, set the log level to `DEBUG` and - '-allowOPTIONS=.*' ``` -### Parameters - -| Parameter | Default Value | Description | -|----------------------|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-allowfrom` | `127.0.0.1/32` | Specifies the IP addresses of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames (comma-separated) can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. | -| `-allowhealthcheck` | (not set) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) | -| `-listenip` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. | -| `-logjson` | (not set) | If set, it enables logging in JSON format. If unset, docker-proxy logs in plain text format. | -| `-loglevel` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. | -| `-proxyport` | `2375` | Defines the TCP port the proxy listens to. | -| `-shutdowngracetime` | `10` | Defines the time in seconds to wait before forcing the shutdown after sigtern or sigint (socket-proxy first tries to graceful shut down the TCP server) | -| `-socketpath` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. | -| `-stoponwatchdog` | (not set) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. | -| `-watchdoginterval` | `0` | Check for socket availabibity every x seconds (disable checks, if not set or value is 0) | +### all parameters and environment variables + +socket-proxy can be configured via command line parameters or via environment variables. If both command line parameter and environment variables are set, the environment variable will be ignored. + +| Parameter | Environment Variable | Default Value | Description | +|----------------------|-------------------------|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames (comma-separated) can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. | +| `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) | +| `-listenip` | `SP_LISTENIP` | `127.0.0.1` | Specifies the IP address the server will bind on. Default is only the internal network. | +| `-logjson` | `SP_LOGJSON` | (not set) | If set, it enables logging in JSON format. If unset, docker-proxy logs in plain text format. | +| `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. | +| `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. | +| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after sigtern or sigint (socket-proxy first tries to graceful shut down the TCP server) | +| `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. | +| `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. | +| `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availabibity every x seconds (disable checks, if not set or value is 0) | ### Changelog @@ -175,6 +185,8 @@ To log the API calls of the client application, set the log level to `DEBUG` and 1.3 - allow multiple, comma-separated hostnames in `-allowfrom` parameter +1.4 - allow configuration from env variables + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/cmd/socket-proxy/handlehttprequest.go b/cmd/socket-proxy/handlehttprequest.go index 6876858..1f203cc 100644 --- a/cmd/socket-proxy/handlehttprequest.go +++ b/cmd/socket-proxy/handlehttprequest.go @@ -2,7 +2,6 @@ package main import ( "errors" - "github.com/wollomatic/socket-proxy/internal/config" "log/slog" "net" "net/http" @@ -25,7 +24,7 @@ func handleHttpRequest(w http.ResponseWriter, r *http.Request) { } // check if the request is allowed - allowed, exists := config.AllowedRequests[r.Method] + allowed, exists := cfg.AllowedRequests[r.Method] if !exists { // method not in map -> not allowed communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed) return diff --git a/cmd/socket-proxy/main.go b/cmd/socket-proxy/main.go index 0320488..6428360 100644 --- a/cmd/socket-proxy/main.go +++ b/cmd/socket-proxy/main.go @@ -18,7 +18,8 @@ import ( ) const ( - programUrl = "github.com/wollomatic/socket-proxy" + programUrl = "github.com/wollomatic/socket-proxy" + logAddSource = false // set to true to log the source position (file and line) of the log message ) var ( @@ -37,7 +38,7 @@ func main() { // setup logging logOpts := &slog.HandlerOptions{ - AddSource: config.LogSourcePosition, + AddSource: logAddSource, Level: cfg.LogLevel, } var logger *slog.Logger @@ -59,14 +60,14 @@ func main() { // print request allow list if cfg.LogJSON { - for method, regex := range config.AllowedRequests { + for method, regex := range cfg.AllowedRequests { slog.Info("configured allowed request", "method", method, "regex", regex) } } else { // don't use slog here, as we want to print the regexes as they are // see https://github.com/wollomatic/socket-proxy/issues/11 fmt.Printf("Request allowlist:\n %-8s %s\n", "Method", "Regex") - for method, regex := range config.AllowedRequests { + for method, regex := range cfg.AllowedRequests { fmt.Printf(" %-8s %s\n", method, regex) } } diff --git a/go.mod b/go.mod index 9fba7ec..aa43515 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/wollomatic/socket-proxy -go 1.22.2 +go 1.22.6 diff --git a/internal/config/config.go b/internal/config/config.go index 87cca41..d23dcbd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,19 +7,19 @@ import ( "log/slog" "net" "net/http" + "os" "regexp" + "strconv" "strings" ) -const LogSourcePosition = false // set to true to log the source position (file and line) of the log message - -const ( +var ( defaultAllowFrom = "127.0.0.1/32" // allowed IPs to connect to the proxy defaultAllowHealthcheck = false // allow health check requests (HEAD http://localhost:55555/health) defaultLogJSON = false // if true, log in JSON format defaultLogLevel = "INFO" // log level as string defaultListenIP = "127.0.0.1" // ip address to bind the server to - defaultProxyPort = 2375 // tcp port to listen on + defaultProxyPort = uint(2375) // tcp port to listen on defaultSocketPath = "/var/run/docker.sock" // path to the unix socket defaultShutdownGraceTime = uint(10) // Maximum time in seconds to wait for the server to shut down gracefully defaultWatchdogInterval = uint(0) // watchdog interval in seconds (0 to disable) @@ -27,6 +27,7 @@ const ( ) type Config struct { + AllowedRequests map[string]*regexp.Regexp AllowFrom string AllowHealthcheck bool LogJSON bool @@ -38,19 +39,17 @@ type Config struct { SocketPath string } -var ( - AllowedRequests map[string]*regexp.Regexp -) - // used for list of allowed requests type methodRegex struct { - method string - regexString string + method string + regexStringFromEnv string + regexStringFromParam string } // mr is the allowlist of requests per http method -// default: regegString is empty, so regexCompiled stays nil and the request is blocked -// if regexString is set with a command line parameter, all requests matching the method and path matching the regex are allowed +// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked +// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed +// else if regexStringEnv from Environment ist checked var mr = []methodRegex{ {method: http.MethodGet}, {method: http.MethodHead}, @@ -70,6 +69,55 @@ func InitConfig() (*Config, error) { proxyPort uint logLevel string ) + + if val, ok := os.LookupEnv("SP_ALLOWFROM"); ok && val != "" { + defaultAllowFrom = val + } + if val, ok := os.LookupEnv("SP_ALLOWHEALTHCHECK"); ok { + if parsedVal, err := strconv.ParseBool(val); err == nil { + defaultAllowHealthcheck = parsedVal + } + } + if val, ok := os.LookupEnv("SP_LOGJSON"); ok { + if parsedVal, err := strconv.ParseBool(val); err == nil { + defaultLogJSON = parsedVal + } + } + if val, ok := os.LookupEnv("SP_LISTENIP"); ok && val != "" { + defaultListenIP = val + } + if val, ok := os.LookupEnv("SP_LOGLEVEL"); ok && val != "" { + defaultLogLevel = val + } + if val, ok := os.LookupEnv("SP_PROXYPORT"); ok && val != "" { + if parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil { + defaultProxyPort = uint(parsedVal) + } + } + if val, ok := os.LookupEnv("SP_SHUTDOWNGRACETIME"); ok && val != "" { + if parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil { + defaultShutdownGraceTime = uint(parsedVal) + } + } + if val, ok := os.LookupEnv("SP_SOCKETPATH"); ok && val != "" { + defaultSocketPath = val + } + if val, ok := os.LookupEnv("SP_STOPONWATCHDOG"); ok { + if parsedVal, err := strconv.ParseBool(val); err == nil { + defaultStopOnWatchdog = parsedVal + } + } + if val, ok := os.LookupEnv("SP_WATCHDOGINTERVAL"); ok && val != "" { + if parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil { + defaultWatchdogInterval = uint(parsedVal) + } + } + for i := 0; i < len(mr); i++ { + if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" { + mr[i].regexStringFromEnv = val + } + } + flag.StringVar(&cfg.AllowFrom, "allowfrom", defaultAllowFrom, "allowed IPs or hostname to connect to the proxy") flag.BoolVar(&cfg.AllowHealthcheck, "allowhealthcheck", defaultAllowHealthcheck, "allow health check requests (HEAD http://localhost:55555/health)") flag.BoolVar(&cfg.LogJSON, "logjson", defaultLogJSON, "log in JSON format (otherwise log in plain text") @@ -81,11 +129,11 @@ func InitConfig() (*Config, error) { flag.BoolVar(&cfg.StopOnWatchdog, "stoponwatchdog", defaultStopOnWatchdog, "stop the program when the socket gets unavailable (otherwise log only)") flag.UintVar(&cfg.WatchdogInterval, "watchdoginterval", defaultWatchdogInterval, "watchdog interval in seconds (0 to disable)") for i := 0; i < len(mr); i++ { - flag.StringVar(&mr[i].regexString, "allow"+mr[i].method, mr[i].regexString, "regex for "+mr[i].method+" requests (not set means method is not allowed)") + flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)") } flag.Parse() - // pcheck listenIP and proxyPort + // check listenIP and proxyPort if net.ParseIP(listenIP) == nil { return nil, fmt.Errorf("invalid IP \"%s\" for listenip", listenIP) } @@ -109,14 +157,20 @@ func InitConfig() (*Config, error) { } // compile regexes for allowed requests - AllowedRequests = make(map[string]*regexp.Regexp) + cfg.AllowedRequests = make(map[string]*regexp.Regexp) for _, rx := range mr { - if rx.regexString != "" { - r, err := regexp.Compile("^" + rx.regexString + "$") + if rx.regexStringFromParam != "" { + r, err := regexp.Compile("^" + rx.regexStringFromParam + "$") + if err != nil { + return nil, fmt.Errorf("invalid regex \"%s\" for method %s in command line parameter: %s", rx.regexStringFromParam, rx.method, err) + } + cfg.AllowedRequests[rx.method] = r + } else if rx.regexStringFromEnv != "" { + r, err := regexp.Compile("^" + rx.regexStringFromEnv + "$") if err != nil { - return nil, fmt.Errorf("invalid regex \"%s\" for method %s: %s", rx.regexString, rx.method, err) + return nil, fmt.Errorf("invalid regex \"%s\" for method %s in env variable: %s", rx.regexStringFromParam, rx.method, err) } - AllowedRequests[rx.method] = r + cfg.AllowedRequests[rx.method] = r } } return &cfg, nil