From d9c66b2ca1377b448cf3e7624a284d91e2d754ff Mon Sep 17 00:00:00 2001 From: wollomatic Date: Wed, 7 Aug 2024 19:15:44 +0200 Subject: [PATCH 1/4] Remove inconsistence in configuration --- cmd/socket-proxy/handlehttprequest.go | 3 +-- cmd/socket-proxy/main.go | 9 +++++---- internal/config/config.go | 13 ++++--------- 3 files changed, 10 insertions(+), 15 deletions(-) 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/internal/config/config.go b/internal/config/config.go index 87cca41..a8f717b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,8 +11,6 @@ import ( "strings" ) -const LogSourcePosition = false // set to true to log the source position (file and line) of the log message - const ( defaultAllowFrom = "127.0.0.1/32" // allowed IPs to connect to the proxy defaultAllowHealthcheck = false // allow health check requests (HEAD http://localhost:55555/health) @@ -27,6 +25,7 @@ const ( ) type Config struct { + AllowedRequests map[string]*regexp.Regexp AllowFrom string AllowHealthcheck bool LogJSON bool @@ -38,10 +37,6 @@ type Config struct { SocketPath string } -var ( - AllowedRequests map[string]*regexp.Regexp -) - // used for list of allowed requests type methodRegex struct { method string @@ -85,7 +80,7 @@ func InitConfig() (*Config, error) { } 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 +104,14 @@ 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 err != nil { return nil, fmt.Errorf("invalid regex \"%s\" for method %s: %s", rx.regexString, rx.method, err) } - AllowedRequests[rx.method] = r + cfg.AllowedRequests[rx.method] = r } } return &cfg, nil From f4ef4213ec786e03bf0a12e71d4ee062941c8517 Mon Sep 17 00:00:00 2001 From: wollomatic Date: Thu, 8 Aug 2024 20:56:30 +0200 Subject: [PATCH 2/4] add possibility to configure socket-proxy via environment variables --- README.md | 42 +++++++++++++-------- internal/config/config.go | 79 ++++++++++++++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 25a912b..77860d0 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 (commandline): + `'-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/). @@ -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 + +| 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) | + +If both commandline parameter and environment variables are set, the environment variable will be ignored. ### 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/internal/config/config.go b/internal/config/config.go index a8f717b..d23dcbd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,17 +7,19 @@ import ( "log/slog" "net" "net/http" + "os" "regexp" + "strconv" "strings" ) -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) @@ -39,13 +41,15 @@ type Config struct { // 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}, @@ -65,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") @@ -76,7 +129,7 @@ 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() @@ -106,10 +159,16 @@ func InitConfig() (*Config, error) { // compile regexes for allowed requests 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) } cfg.AllowedRequests[rx.method] = r } From 3d2a17b61b826fb02356f261cbf7d8ef8b897422 Mon Sep 17 00:00:00 2001 From: wollomatic Date: Thu, 8 Aug 2024 21:06:43 +0200 Subject: [PATCH 3/4] make documentation (hopefully) a bit less unclear --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 77860d0..5a8fcf5 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ If both commandline parameter and environment variable is configured for a parti 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 (commandline): +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. @@ -78,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 # [...] @@ -91,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 @@ -160,6 +160,8 @@ To log the API calls of the client application, set the log level to `DEBUG` and ### 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. | @@ -173,8 +175,6 @@ To log the API calls of the client application, set the log level to `DEBUG` and | `-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) | -If both commandline parameter and environment variables are set, the environment variable will be ignored. - ### Changelog 1.0 - initial release From 2cf1be3093614ad1d17a01a3714f09b674b7ffaa Mon Sep 17 00:00:00 2001 From: wollomatic Date: Sat, 10 Aug 2024 12:07:08 +0200 Subject: [PATCH 4/4] Update to Go 1.22.6 --- Dockerfile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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