Skip to content
Draft
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
104 changes: 104 additions & 0 deletions cmd/src/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"flag"
"fmt"
"io"
"log"
"os"

"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/src-cli/internal/cmderrors"
"github.com/sourcegraph/src-cli/internal/srcproxy"
)

func init() {
// NOTE: this is an experimental command. It isn't advertised in -help

flagSet := flag.NewFlagSet("proxy", flag.ExitOnError)
usageFunc := func() {
fmt.Fprintf(flag.CommandLine.Output(), `'src proxy' starts a local reverse proxy to your Sourcegraph instance.

USAGE
src [-v] proxy [-addr :7777] [-insecure-skip-verify] [-server-cert cert.pem -server-key key.pem] [-log-file path] [client-ca.pem]

By default, proxied requests use SRC_ACCESS_TOKEN via:
Authorization: token SRC_ACCESS_TOKEN

If a client CA certificate path is provided, proxy runs in mTLS sudo mode:
1. Serves HTTPS and requires a client certificate signed by the provided CA.
2. Reads the first email SAN from the presented client certificate.
3. Looks up the Sourcegraph user by that email.
4. Proxies requests with:
Authorization: token-sudo token="TOKEN",user="USERNAME"

Server certificate options:
-server-cert and -server-key can be used to provide the TLS certificate
and key used by the local proxy server. If omitted in cert mode, an
ephemeral self-signed server certificate is generated.
`)
}

var (
addrFlag = flagSet.String("addr", ":7777", "Address on which to serve")
insecureSkipVerifyFlag = flagSet.Bool("insecure-skip-verify", false, "Skip validation of TLS certificates against trusted chains")
serverCertFlag = flagSet.String("server-cert", "", "Path to TLS server certificate for local proxy listener")
serverKeyFlag = flagSet.String("server-key", "", "Path to TLS server private key for local proxy listener")
logFileFlag = flagSet.String("log-file", "", "Path to log file. If not set, logs are written to stderr")
)

handler := func(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}

var clientCAPath string
switch flagSet.NArg() {
case 0:
case 1:
clientCAPath = flagSet.Arg(0)
default:
return cmderrors.Usage("requires zero or one positional argument: path to client CA certificate")
}
if (*serverCertFlag == "") != (*serverKeyFlag == "") {
return cmderrors.Usage("both -server-cert and -server-key must be provided together")
}

logOutput := io.Writer(os.Stderr)
var logF *os.File
if *logFileFlag != "" {
var err error
logF, err = os.Create(*logFileFlag)
if err != nil {
return errors.Wrap(err, "open log file")
}
defer func() { _ = logF.Close() }()
logOutput = logF
}

dbug := log.New(io.Discard, "", log.LstdFlags)
if *verbose {
dbug = log.New(logOutput, "DBUG proxy: ", log.LstdFlags)
}

s := &srcproxy.Serve{
Addr: *addrFlag,
Endpoint: cfg.Endpoint,
AccessToken: cfg.AccessToken,
ClientCAPath: clientCAPath,
ServerCertPath: *serverCertFlag,
ServerKeyPath: *serverKeyFlag,
InsecureSkipVerify: *insecureSkipVerifyFlag,
AdditionalHeaders: cfg.AdditionalHeaders,
Info: log.New(logOutput, "proxy: ", log.LstdFlags),
Debug: dbug,
}
return s.Start()
}

commands = append(commands, &command{
flagSet: flagSet,
handler: handler,
usageFunc: usageFunc,
})
}
71 changes: 71 additions & 0 deletions internal/srcproxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# srcproxy

`src proxy` is a local reverse proxy for Sourcegraph with two auth modes.

## Auth Modes

- Default mode (no CA arg): forwards requests with `Authorization: token <SRC_ACCESS_TOKEN>`.
- mTLS sudo mode (with CA arg): requires client certificate, extracts first email SAN, resolves Sourcegraph user by email, then forwards with `token-sudo`.

## Run

```bash
# default mode
src proxy

# mTLS sudo mode
src -v proxy \
-server-cert ./internal/srcproxy/test-certs/server.pem \
-server-key ./internal/srcproxy/test-certs/server.key \
./internal/srcproxy/test-certs/ca.pem
```

## Logging

- `-v` enables request-level debug logging.
- `-log-file <path>` writes logs to a file.
- Without `-log-file`, logs go to stderr.

Example:

```bash
src -v proxy -log-file ./proxy.log ./internal/srcproxy/test-certs/ca.pem
```

## Request Format

GraphQL requests should use JSON:

```bash
curl -k \
-H 'Content-Type: application/json' \
--cert ./internal/srcproxy/test-certs/client.pem \
--key ./internal/srcproxy/test-certs/client.key \
https://localhost:7777/.api/graphql \
-d '{"query":"{ currentUser { username } }"}'
```

If `Content-Type` is omitted with `curl -d`, curl sends `application/x-www-form-urlencoded`, which Sourcegraph GraphQL rejects.

## Important Routing Behavior

The proxy rewrites upstream `Host` to `SRC_ENDPOINT` host.

This is required for name-based routing (for example Caddy virtual hosts). If `Host` is forwarded as `localhost:<proxy-port>`, some upstream setups return `200` with empty body from a default vhost instead of Sourcegraph GraphQL.

## mTLS Certificate Requirements

- Client cert must chain to the CA file passed as positional arg.
- Client cert must include an email SAN.
- The email SAN must map to an existing Sourcegraph user.

## Troubleshooting

- `HTTP 200` with empty body:
upstream host routing mismatch. Confirm proxy is current and `Host` rewrite is in place.
- `no client certificate presented`:
client did not send cert/key or CA trust does not match.
- `client certificate does not contain an email SAN`:
regenerate client cert with email SAN.
- `no Sourcegraph user found for certificate email`:
cert email is not a Sourcegraph user email.
86 changes: 86 additions & 0 deletions internal/srcproxy/gen-test-certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# gen-test-certs.sh — generate certs for testing `src proxy` mTLS mode
#
# Usage: ./gen-test-certs.sh [email] [output-dir]
# email: email SAN to embed in client cert (default: alice@example.com)
# output-dir: where to write files (default: ./test-certs)
set -euo pipefail

EMAIL="${1:-alice@example.com}"
DIR="${2:-./test-certs}"
mkdir -p "$DIR"

echo "==> Generating certs in $DIR (email: $EMAIL)"

# ── 1. CA ────────────────────────────────────────────────────────────────────
openssl genrsa -out "$DIR/ca.key" 2048 2>/dev/null

openssl req -new -x509 -days 1 \
-key "$DIR/ca.key" \
-out "$DIR/ca.pem" \
-subj "/CN=Test Client CA" 2>/dev/null

echo " ca.pem / ca.key"

# ── 2. Server cert (so you can pass it to the proxy and trust it in curl) ────
openssl genrsa -out "$DIR/server.key" 2048 2>/dev/null

openssl req -new \
-key "$DIR/server.key" \
-out "$DIR/server.csr" \
-subj "/CN=localhost" 2>/dev/null

openssl x509 -req -days 1 \
-in "$DIR/server.csr" \
-signkey "$DIR/server.key" \
-out "$DIR/server.pem" \
-extfile <(printf 'subjectAltName=DNS:localhost,IP:127.0.0.1') 2>/dev/null

echo " server.pem / server.key"

# ── 3. Client cert with email SAN signed by the CA ───────────────────────────
openssl genrsa -out "$DIR/client.key" 2048 2>/dev/null

openssl req -new \
-key "$DIR/client.key" \
-out "$DIR/client.csr" \
-subj "/CN=test-client" 2>/dev/null

openssl x509 -req -days 1 \
-in "$DIR/client.csr" \
-CA "$DIR/ca.pem" \
-CAkey "$DIR/ca.key" \
-CAcreateserial \
-out "$DIR/client.pem" \
-extfile <(printf "subjectAltName=email:%s" "$EMAIL") 2>/dev/null

echo " client.pem / client.key (email SAN: $EMAIL)"

# Confirm the SAN is present
echo ""
echo "==> Verifying email SAN in client cert:"
openssl x509 -in "$DIR/client.pem" -noout -text \
| grep -A1 "Subject Alternative Name"

echo ""
echo "==> Done. Next steps:"
echo ""
echo " # 1. Start the proxy (in another terminal):"
echo " export SRC_ENDPOINT=https://sourcegraph.example.com"
echo " export SRC_ACCESS_TOKEN=<site-admin-sudo-token>"
echo " go run ./cmd/src proxy \\"
echo " -server-cert $DIR/server.pem \\"
echo " -server-key $DIR/server.key \\"
echo " $DIR/ca.pem"
echo ""
echo " # 2. Send a request via curl using the client cert:"
echo " curl --cacert $DIR/server.pem \\"
echo " --cert $DIR/client.pem \\"
echo " --key $DIR/client.key \\"
echo " https://localhost:7777/.api/graphql \\"
echo " -d '{\"query\":\"{ currentUser { username } }\"}'"
echo ""
echo " # Or skip server cert verification with -k:"
echo " curl -k --cert $DIR/client.pem --key $DIR/client.key \\"
echo " https://localhost:7777/.api/graphql \\"
echo " -d '{\"query\":\"{ currentUser { username } }\"}'"
Loading
Loading