Skip to content

Commit 4bd40aa

Browse files
committed
feat: "proxy" subcommand providing an mTLS authenticating proxy
This implements a feature a few customers have already implemented internally. They rely on mTLS to avoid the need of distributing access tokens per user / oauth. This proxy uses the email field of a request and then sets the appropriate headers on the request for sourcegraph to authenticate it. In particular we rely on the site-admin:sudo scope which allows user impersonation. This feature is still experimental so is not shown in the -help output.
1 parent ad16cda commit 4bd40aa

File tree

4 files changed

+759
-0
lines changed

4 files changed

+759
-0
lines changed

cmd/src/proxy.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io"
7+
"log"
8+
"os"
9+
10+
"github.com/sourcegraph/src-cli/internal/cmderrors"
11+
"github.com/sourcegraph/src-cli/internal/srcproxy"
12+
)
13+
14+
func init() {
15+
// NOTE: this is an experimental command. It isn't advertised in -help
16+
17+
flagSet := flag.NewFlagSet("proxy", flag.ExitOnError)
18+
usageFunc := func() {
19+
fmt.Fprintf(flag.CommandLine.Output(), `'src proxy' starts a local reverse proxy to your Sourcegraph instance.
20+
21+
USAGE
22+
src [-v] proxy [-addr :7777] [-insecure-skip-verify] [-server-cert cert.pem -server-key key.pem] [client-ca.pem]
23+
24+
By default, proxied requests use SRC_ACCESS_TOKEN via:
25+
Authorization: token SRC_ACCESS_TOKEN
26+
27+
If a client CA certificate path is provided, proxy runs in mTLS sudo mode:
28+
1. Serves HTTPS and requires a client certificate signed by the provided CA.
29+
2. Reads the first email SAN from the presented client certificate.
30+
3. Looks up the Sourcegraph user by that email.
31+
4. Proxies requests with:
32+
Authorization: token-sudo token="TOKEN",user="USERNAME"
33+
34+
Server certificate options:
35+
-server-cert and -server-key can be used to provide the TLS certificate
36+
and key used by the local proxy server. If omitted in cert mode, an
37+
ephemeral self-signed server certificate is generated.
38+
`)
39+
}
40+
41+
var (
42+
addrFlag = flagSet.String("addr", ":7777", "Address on which to serve")
43+
insecureSkipVerifyFlag = flagSet.Bool("insecure-skip-verify", false, "Skip validation of TLS certificates against trusted chains")
44+
serverCertFlag = flagSet.String("server-cert", "", "Path to TLS server certificate for local proxy listener")
45+
serverKeyFlag = flagSet.String("server-key", "", "Path to TLS server private key for local proxy listener")
46+
)
47+
48+
handler := func(args []string) error {
49+
if err := flagSet.Parse(args); err != nil {
50+
return err
51+
}
52+
53+
var clientCAPath string
54+
switch flagSet.NArg() {
55+
case 0:
56+
case 1:
57+
clientCAPath = flagSet.Arg(0)
58+
default:
59+
return cmderrors.Usage("requires zero or one positional argument: path to client CA certificate")
60+
}
61+
if (*serverCertFlag == "") != (*serverKeyFlag == "") {
62+
return cmderrors.Usage("both -server-cert and -server-key must be provided together")
63+
}
64+
65+
dbug := log.New(io.Discard, "", log.LstdFlags)
66+
if *verbose {
67+
dbug = log.New(os.Stderr, "DBUG proxy: ", log.LstdFlags)
68+
}
69+
70+
s := &srcproxy.Serve{
71+
Addr: *addrFlag,
72+
Endpoint: cfg.Endpoint,
73+
AccessToken: cfg.AccessToken,
74+
ClientCAPath: clientCAPath,
75+
ServerCertPath: *serverCertFlag,
76+
ServerKeyPath: *serverKeyFlag,
77+
InsecureSkipVerify: *insecureSkipVerifyFlag,
78+
AdditionalHeaders: cfg.AdditionalHeaders,
79+
Info: log.New(os.Stderr, "proxy: ", log.LstdFlags),
80+
Debug: dbug,
81+
}
82+
return s.Start()
83+
}
84+
85+
commands = append(commands, &command{
86+
flagSet: flagSet,
87+
handler: handler,
88+
usageFunc: usageFunc,
89+
})
90+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
# gen-test-certs.sh — generate certs for testing `src proxy` mTLS mode
3+
#
4+
# Usage: ./gen-test-certs.sh [email] [output-dir]
5+
# email: email SAN to embed in client cert (default: alice@example.com)
6+
# output-dir: where to write files (default: ./test-certs)
7+
set -euo pipefail
8+
9+
EMAIL="${1:-alice@example.com}"
10+
DIR="${2:-./test-certs}"
11+
mkdir -p "$DIR"
12+
13+
echo "==> Generating certs in $DIR (email: $EMAIL)"
14+
15+
# ── 1. CA ────────────────────────────────────────────────────────────────────
16+
openssl genrsa -out "$DIR/ca.key" 2048 2>/dev/null
17+
18+
openssl req -new -x509 -days 1 \
19+
-key "$DIR/ca.key" \
20+
-out "$DIR/ca.pem" \
21+
-subj "/CN=Test Client CA" 2>/dev/null
22+
23+
echo " ca.pem / ca.key"
24+
25+
# ── 2. Server cert (so you can pass it to the proxy and trust it in curl) ────
26+
openssl genrsa -out "$DIR/server.key" 2048 2>/dev/null
27+
28+
openssl req -new \
29+
-key "$DIR/server.key" \
30+
-out "$DIR/server.csr" \
31+
-subj "/CN=localhost" 2>/dev/null
32+
33+
openssl x509 -req -days 1 \
34+
-in "$DIR/server.csr" \
35+
-signkey "$DIR/server.key" \
36+
-out "$DIR/server.pem" \
37+
-extfile <(printf 'subjectAltName=DNS:localhost,IP:127.0.0.1') 2>/dev/null
38+
39+
echo " server.pem / server.key"
40+
41+
# ── 3. Client cert with email SAN signed by the CA ───────────────────────────
42+
openssl genrsa -out "$DIR/client.key" 2048 2>/dev/null
43+
44+
openssl req -new \
45+
-key "$DIR/client.key" \
46+
-out "$DIR/client.csr" \
47+
-subj "/CN=test-client" 2>/dev/null
48+
49+
openssl x509 -req -days 1 \
50+
-in "$DIR/client.csr" \
51+
-CA "$DIR/ca.pem" \
52+
-CAkey "$DIR/ca.key" \
53+
-CAcreateserial \
54+
-out "$DIR/client.pem" \
55+
-extfile <(printf "subjectAltName=email:%s" "$EMAIL") 2>/dev/null
56+
57+
echo " client.pem / client.key (email SAN: $EMAIL)"
58+
59+
# Confirm the SAN is present
60+
echo ""
61+
echo "==> Verifying email SAN in client cert:"
62+
openssl x509 -in "$DIR/client.pem" -noout -text \
63+
| grep -A1 "Subject Alternative Name"
64+
65+
echo ""
66+
echo "==> Done. Next steps:"
67+
echo ""
68+
echo " # 1. Start the proxy (in another terminal):"
69+
echo " export SRC_ENDPOINT=https://sourcegraph.example.com"
70+
echo " export SRC_ACCESS_TOKEN=<site-admin-sudo-token>"
71+
echo " go run ./cmd/src proxy \\"
72+
echo " -server-cert $DIR/server.pem \\"
73+
echo " -server-key $DIR/server.key \\"
74+
echo " $DIR/ca.pem"
75+
echo ""
76+
echo " # 2. Send a request via curl using the client cert:"
77+
echo " curl --cacert $DIR/server.pem \\"
78+
echo " --cert $DIR/client.pem \\"
79+
echo " --key $DIR/client.key \\"
80+
echo " https://localhost:7777/.api/graphql \\"
81+
echo " -d '{\"query\":\"{ currentUser { username } }\"}'"
82+
echo ""
83+
echo " # Or skip server cert verification with -k:"
84+
echo " curl -k --cert $DIR/client.pem --key $DIR/client.key \\"
85+
echo " https://localhost:7777/.api/graphql \\"
86+
echo " -d '{\"query\":\"{ currentUser { username } }\"}'"

0 commit comments

Comments
 (0)