Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
# Security Policy

## Supported Versions
## Reporting a Vulnerability

Use this section to tell people about which versions of your project are
currently being supported with security updates.
Please report security issues privately via GitHub's "Report a vulnerability"
button on the Security tab of this repository. We aim to acknowledge new
reports within 5 business days.

| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Default Security Posture

## Reporting a Vulnerability
Greyproxy is designed to run on the same host as the workloads it proxies. By
default it binds **only to the loopback interface (`127.0.0.1`)** on every
port:

| Service | Default bind |
|---------------|---------------------|
| Dashboard/API | `127.0.0.1:43080` |
| HTTP Proxy | `127.0.0.1:43051` |
| SOCKS5 Proxy | `127.0.0.1:43052` |
| DNS Proxy | `127.0.0.1:43053` |

This prevents the dashboard, REST API, and proxy ports from being reachable
from other machines on the network without explicit operator action — the
proxies cannot be abused as an open relay, the DNS resolver cannot be used
for amplification, and the management API cannot be reached by other hosts.

Use this section to tell people how to report a vulnerability.
To expose greyproxy on a network interface, either:

- pass `--host <ip>` to `greyproxy serve` (IP literal only; hostnames are
rejected), or
- set the top-level `host:` field in the config file.

The CLI flag wins over the YAML field. Explicit hosts in individual `addr:`
entries (e.g. `addr: "0.0.0.0:43080"`) are honoured as-is.

When the resolved host is an unspecified address (`0.0.0.0` or `::`),
greyproxy logs a warning at startup so the choice is visible in the logs.

## Supported Versions

Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
Security fixes target the latest tagged release.
8 changes: 8 additions & 0 deletions cmd/greyproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"sync"

"github.com/greyhavenhq/greyproxy/internal/gostcore/logger"
"github.com/greyhavenhq/greyproxy/internal/gostx/config/parsing/parser"
xlogger "github.com/greyhavenhq/greyproxy/internal/gostx/logger"
"github.com/kardianos/service"
)
Expand All @@ -38,6 +39,7 @@ var (
silentAllow bool
middlewareURLFlags stringList
middlewareCmdFlags stringList
hostFlag string
)

func init() {
Expand Down Expand Up @@ -98,8 +100,14 @@ func parseFlags() {
flag.BoolVar(&silentAllow, "silent-allow", false, "activate silent allow-all mode until restart")
flag.Var(&middlewareURLFlags, "middleware", "middleware service URL (ws:// or http://); repeatable, cascades in declaration order")
flag.Var(&middlewareCmdFlags, "middleware-cmd", "command to spawn as a stdio middleware (e.g. 'uv run mw.py'); repeatable, cascades after --middleware entries")
flag.StringVar(&hostFlag, "host", "", "default bind interface for listeners written as a bare port (e.g. \":43080\"); IP literal only, defaults to 127.0.0.1")
flag.Parse()

if _, err := parser.ParseHostFlag(hostFlag); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "--host: %v\n", err)
os.Exit(2)
}

// Normalize http(s):// to ws(s):// for each middleware URL
for i, u := range middlewareURLFlags {
switch {
Expand Down
25 changes: 25 additions & 0 deletions cmd/greyproxy/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (p *program) initParser() {
Debug: debug,
Trace: trace,
MetricsAddr: metricsAddr,
Host: hostFlag,
})
}

Expand All @@ -82,6 +83,8 @@ func (p *program) Start(s service.Service) error {
os.Exit(0)
}

warnIfUnspecifiedBind(parser.ResolveHost(hostFlag, viper.GetString("host")))

// Auto-inject MITM cert paths if CA files exist
injectCertPaths(cfg, greyproxyDataHome())

Expand Down Expand Up @@ -272,6 +275,7 @@ func (p *program) run(cfg *config.Config) error {
if addr == "" {
addr = ":6060"
}
addr = parser.ApplyDefaultHost(addr, parser.ResolveHost(hostFlag, viper.GetString("host")))
s := &http.Server{
Addr: addr,
}
Expand Down Expand Up @@ -410,6 +414,13 @@ func (p *program) buildGreyproxyService() error {
gaCfg.Resolver = "resolver-0"
}

// Normalize the dashboard bind address. The service / metrics / profiling
// addrs are normalized inside parser.Parse() via walkAddresses, but the
// greyproxy block lives under a viper subtree and is unmarshalled here,
// so it needs its own pass against the same resolved host.
resolvedHost := parser.ResolveHost(hostFlag, viper.GetString("host"))
gaCfg.Addr = parser.ApplyDefaultHost(gaCfg.Addr, resolvedHost)

applyDockerEnvOverrides(&gaCfg)

log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@greyproxy"})
Expand Down Expand Up @@ -1173,6 +1184,20 @@ func decompressWebSocketFrame(payload []byte) ([]byte, error) {
return io.ReadAll(r)
}

// warnIfUnspecifiedBind logs once at startup when the resolved bind host is an
// unspecified address (0.0.0.0 or ::). Operators see the warning in logs and
// can confirm the choice was intentional.
func warnIfUnspecifiedBind(host string) {
ip := net.ParseIP(host)
if ip == nil || !ip.IsUnspecified() {
return
}
logger.Default().Warnf(
"binding listeners to all interfaces (host=%s); proxy and dashboard will be reachable from any network — see SECURITY.md",
host,
)
}

// applyDockerEnvOverrides configures Docker resolution from environment variables.
// Docker is disabled by default; use these env vars to opt in:
//
Expand Down
3 changes: 2 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ greyproxy serve -C greyproxy.yml
| `-O <format>` | Dump the resolved config and exit. One of `yaml` or `json`. |
| `-metrics <addr>` | Expose Prometheus-style metrics on the given address. |
| `-silent-allow` | Enter silent allow-all mode at startup. Requests pass through without prompting until the process restarts. |
| `--host <ip>` | Default bind interface for listeners written as a bare port (e.g. `:43080`). IP literal only (hostnames are rejected). Defaults to `127.0.0.1`. Pass `--host 0.0.0.0` to bind every interface — the choice is logged with a warning at startup. |
| `-V` | Print version information and exit. |

### `greyproxy cert`
Expand Down Expand Up @@ -71,7 +72,7 @@ greyproxy install -f # skip confirmation prompts
|------|-------------|
| `-f`, `--force` | Skip interactive confirmation prompts. |

The dashboard is available at [http://localhost:43080](http://localhost:43080) once the service is running.
The dashboard is available at [http://localhost:43080](http://localhost:43080) once the service is running. The installed service binds to loopback only; to expose it on a network interface, edit the systemd unit (Linux) or launchd plist (macOS) to add `--host <ip>` to the `serve` command line.

### `greyproxy uninstall`

Expand Down
26 changes: 26 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ The config format is inherited from [GOST v3](https://gost.run/en/); greyproxy a
## Example Configuration

```yaml
# Default bind interface for any listener written as a bare port (":43080").
# Loopback by default so the proxy and dashboard aren't reachable from other
# machines. See "Bind Interface" below for details.
host: 127.0.0.1

log:
level: info
format: json
Expand Down Expand Up @@ -84,6 +89,27 @@ services:
resolver: resolver-0
```

## Bind Interface

By default greyproxy binds **only to the loopback interface** (`127.0.0.1`).
Any listener whose address is a bare port (`":43080"`, `"43080"`) is rewritten
to `127.0.0.1:<port>` at startup. Addresses that already carry an explicit
host (`"0.0.0.0:43080"`, `"192.168.1.10:43080"`, `"[::1]:43080"`) are left
alone.

Override the default at the top of the config file:

```yaml
host: 0.0.0.0 # bind to every interface
```

Or pass `--host <ip>` to `greyproxy serve` (IP literal only; hostnames are
rejected). The CLI flag wins over the YAML field, which wins over the
built-in `127.0.0.1` default.

When the resolved host is unspecified (`0.0.0.0` or `::`), greyproxy logs a
warning at startup so the operator can confirm the choice was deliberate.

## The `greyproxy` Block

| Field | Type | Description |
Expand Down
6 changes: 6 additions & 0 deletions greyproxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
# NOTE: This is a template file. The actual gost.yaml is generated
# by entrypoint-gost.sh which adds custom hosts from PROXY_CUSTOM_HOSTS.

# Default bind interface for any listener written as a bare port (":43080").
# Loopback by default so the proxy and dashboard aren't reachable from other
# machines. Override with `greyproxy serve --host <ip>` (IP literal only) or
# by editing this field. See SECURITY.md for the rationale.
host: 127.0.0.1

log:
level: info
format: json
Expand Down
5 changes: 5 additions & 0 deletions internal/gostx/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,11 @@ type NodeConfig struct {
}

type Config struct {
// Host is the default bind interface for any listener whose addr is a
// bare port (":43080" / "43080"). Defaults to 127.0.0.1; overridden by
// the --host CLI flag. Explicit hosts in service / metrics / profiling
// addrs are left alone.
Host string `yaml:"host,omitempty" json:"host,omitempty"`
Services []*ServiceConfig `json:"services"`
Authers []*AutherConfig `yaml:",omitempty" json:"authers,omitempty"`
Admissions []*AdmissionConfig `yaml:",omitempty" json:"admissions,omitempty"`
Expand Down
90 changes: 90 additions & 0 deletions internal/gostx/config/parsing/parser/host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package parser

import (
"fmt"
"net"
"strconv"
"strings"

"github.com/greyhavenhq/greyproxy/internal/gostx/config"
)

// DefaultHost is the address greyproxy binds to when no host is specified
// in the config or on the command line. Loopback is the safe default — see
// SECURITY.md for the rationale.
const DefaultHost = "127.0.0.1"

// ParseHostFlag validates the value of the --host CLI flag and the top-level
// `host:` YAML field. It accepts IP literals only (IPv4 or IPv6); hostnames
// are rejected so that operators don't have to wonder which resolved address
// the listener picked.
func ParseHostFlag(s string) (string, error) {
if s == "" {
return "", nil
}
if net.ParseIP(s) == nil {
return "", fmt.Errorf("host requires a literal IP address, got %q", s)
}
return s, nil
}

// ApplyDefaultHost prepends host to addr when addr is a bare port form like
// ":43080" or "43080". Addresses that already carry a host part (explicit
// 0.0.0.0, 127.0.0.1, a LAN IP, [::]/[::1], etc.) are returned unchanged so
// operator overrides win. Non-TCP URI forms ("unix://...") and the empty
// string are also returned unchanged.
func ApplyDefaultHost(addr, host string) string {
if addr == "" {
return ""
}
if strings.Contains(addr, "://") {
return addr
}
h, port, err := net.SplitHostPort(addr)
if err != nil {
if _, perr := strconv.Atoi(addr); perr == nil {
return net.JoinHostPort(host, addr)
}
return addr
}
if h != "" {
return addr
}
return net.JoinHostPort(host, port)
}

// walkAddresses applies ApplyDefaultHost to every listener address in cfg:
// each service, the metrics endpoint, and the pprof endpoint. The greyproxy
// dashboard address lives under the `greyproxy:` viper subtree and is
// normalized separately in cmd/greyproxy/program.go.
func walkAddresses(cfg *config.Config, host string) {
if cfg == nil {
return
}
for _, svc := range cfg.Services {
if svc == nil {
continue
}
svc.Addr = ApplyDefaultHost(svc.Addr, host)
}
if cfg.Metrics != nil {
cfg.Metrics.Addr = ApplyDefaultHost(cfg.Metrics.Addr, host)
}
if cfg.Profiling != nil {
cfg.Profiling.Addr = ApplyDefaultHost(cfg.Profiling.Addr, host)
}
}

// IsUnspecifiedBind returns true when addr binds to all interfaces
// (0.0.0.0 / ::), so callers can warn the operator at startup.
func IsUnspecifiedBind(addr string) bool {
if addr == "" {
return false
}
h, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
ip := net.ParseIP(h)
return ip != nil && ip.IsUnspecified()
}
Loading
Loading