Skip to content

Commit 13a604d

Browse files
committed
Add sidecar enrollment workflow
1 parent 06e7166 commit 13a604d

10 files changed

Lines changed: 757 additions & 54 deletions

File tree

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[workspace]
2+
members = [".", "sidecar"]
3+
14
[package]
25
name = "agent_gateway"
36
version = "0.1.0"

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ cargo build --release
1313
## Quick start
1414

1515
```bash
16-
./examples/generate-certs.sh # creates certs/ directory
16+
./examples/generate-certs.sh # server CA + gateway cert under certs/
1717
cp config.example.toml config.toml # edit to taste
18-
cargo run -- --config config.toml
1918
```
2019

21-
The generated filenames match `config.example.toml` so no editing is needed for local development. Pass a custom extension value as an argument: `./examples/generate-certs.sh agent-beta`.
20+
`generate-certs.sh` only creates **server** TLS material (`server-ca.pem`, `server.pem`, …). Each machine (including yours) enrolls with `./examples/connect.sh` (see below), which appends `machine-client-ca.pem` to `client_ca_path`. The gateway needs **at least one** such CA in `certs/client-ca-bundle.pem` before the first start.
21+
22+
Typical first-time flow:
23+
24+
1. `./examples/generate-certs.sh` and `cp config.example.toml config.toml`.
25+
2. `./examples/connect.sh --gateway 127.0.0.1:8443 --gateway-ca certs/server-ca.pem` — it creates `machine-client-ca.pem` under `~/.local/share/agent-gateway/` (or `$XDG_DATA_HOME`). Append that file to `client_ca_path` (e.g. `cat … >> certs/client-ca-bundle.pem`).
26+
3. Start the gateway (`cargo run -- --config config.toml`), then return to the terminal running `connect.sh` and press Enter to start the sidecar and Claude.
27+
28+
On later runs, start the gateway first, run `connect.sh`, and press Enter after confirming the machine CA is still registered (always required after `--regenerate-certs`).
29+
30+
Pass a custom policy extension value: `connect.sh ... --extension-value agent-beta`.
2231

2332
## Configuration
2433

@@ -31,7 +40,7 @@ Copy `config.example.toml` to `config.toml` and edit it. Key sections:
3140
| `listen_addr` | yes | `host:port` to bind (e.g. `0.0.0.0:8443`) |
3241
| `tls_cert_path` | yes | PEM server certificate |
3342
| `tls_key_path` | yes | PEM private key for the server cert |
34-
| `client_ca_path` | yes | PEM CA that issued client certificates |
43+
| `client_ca_path` | yes | PEM bundle of per-machine client CAs (append each `machine-client-ca.pem`) |
3544

3645
**`[policy]`** -- Maps certificate extension values to allowed destinations.
3746

config.example.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
[server]
22
listen_addr = "0.0.0.0:8443"
3-
tls_cert_path = "certs/proxy.pem"
4-
tls_key_path = "certs/proxy-key.pem"
5-
client_ca_path = "certs/client-ca.pem"
3+
tls_cert_path = "certs/server.pem"
4+
tls_key_path = "certs/server-key.pem"
5+
# Concatenated PEM of per-machine client CAs (from connect.sh: machine-client-ca.pem).
6+
# Append one -----BEGIN CERTIFICATE----- block per enrolled machine; restart the gateway to load.
7+
client_ca_path = "certs/client-ca-bundle.pem"
68

79
[observability]
810
log_level = "info"
@@ -15,7 +17,7 @@ client_ext_oid = "1.3.6.1.4.1.57264.1.1"
1517
[[policy.rules]]
1618
extension_value = "agent-alpha"
1719
# Port defaults to 443 when omitted.
18-
allowed_destinations = ["api.example.com", "db.internal.com:5432"]
20+
allowed_destinations = ["api.example.com", "db.internal.com:5432", "api.anthropic.com:443"]
1921

2022
[[policy.rules]]
2123
extension_value = "agent-beta"

examples/connect.sh

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env bash
2+
# Per-machine client certs → $XDG_DATA_HOME/.../agent-gateway; then sidecar + claude. Dev: --gateway-ca certs/server-ca.pem
3+
4+
set -euo pipefail
5+
6+
LISTEN="127.0.0.1:3128"
7+
EXTENSION_VALUE="agent-alpha"
8+
SIDECAR_BIN=""
9+
REGENERATE_CERTS=false
10+
GATEWAY=""
11+
GATEWAY_CA=""
12+
13+
usage() {
14+
cat <<'EOF'
15+
Usage: connect.sh [OPTIONS]
16+
17+
Required:
18+
--gateway HOST:PORT Gateway address
19+
--gateway-ca PATH CA that issued tls_cert_path (dev: certs/server-ca.pem)
20+
21+
Optional:
22+
--extension-value VALUE (default: agent-alpha)
23+
--listen ADDR:PORT
24+
--sidecar-bin PATH
25+
--regenerate-certs
26+
-h, --help
27+
EOF
28+
exit "${1:-0}"
29+
}
30+
31+
need_arg() { [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; }; }
32+
33+
while [[ $# -gt 0 ]]; do
34+
case "$1" in
35+
--gateway) need_arg "$@"; GATEWAY="$2"; shift 2 ;;
36+
--gateway-ca) need_arg "$@"; GATEWAY_CA="$2"; shift 2 ;;
37+
--extension-value) need_arg "$@"; EXTENSION_VALUE="$2"; shift 2 ;;
38+
--listen) need_arg "$@"; LISTEN="$2"; shift 2 ;;
39+
--sidecar-bin) need_arg "$@"; SIDECAR_BIN="$2"; shift 2 ;;
40+
--regenerate-certs) REGENERATE_CERTS=true; shift ;;
41+
-h|--help) usage 0 ;;
42+
*) echo "Unknown option: $1" >&2; usage 1 ;;
43+
esac
44+
done
45+
46+
[[ -n "$GATEWAY" ]] || { echo "error: --gateway is required" >&2; usage 1; }
47+
[[ -n "$GATEWAY_CA" ]] || { echo "error: --gateway-ca is required" >&2; usage 1; }
48+
[[ -f "$GATEWAY_CA" ]] || { echo "error: gateway CA file not found: $GATEWAY_CA" >&2; exit 1; }
49+
50+
CERT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/agent-gateway"
51+
mkdir -p "$CERT_DIR"
52+
53+
der_utf8string() {
54+
local val="$1"
55+
local len=${#val}
56+
local result
57+
result=$(printf '0c:%02x' "$len")
58+
for (( i=0; i<len; i++ )); do
59+
result=$(printf '%s:%02x' "$result" "'${val:$i:1}")
60+
done
61+
printf '%s' "$result"
62+
}
63+
64+
generate_certs() {
65+
echo "==> Generating per-machine client CA"
66+
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
67+
-keyout "$CERT_DIR/machine-client-ca-key.pem" -out "$CERT_DIR/machine-client-ca.pem" \
68+
-days 365 -nodes -subj "/CN=agent-gateway machine client CA" 2>/dev/null
69+
70+
echo "==> Generating machine client certificate (extension_value=$EXTENSION_VALUE)"
71+
local der_hex
72+
der_hex=$(der_utf8string "$EXTENSION_VALUE")
73+
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
74+
-keyout "$CERT_DIR/machine-client-key.pem" -out "$CERT_DIR/machine-client.csr" \
75+
-nodes -subj "/CN=machine-client" 2>/dev/null
76+
openssl x509 -req -in "$CERT_DIR/machine-client.csr" \
77+
-CA "$CERT_DIR/machine-client-ca.pem" -CAkey "$CERT_DIR/machine-client-ca-key.pem" -CAcreateserial \
78+
-out "$CERT_DIR/machine-client.pem" -days 365 \
79+
-extfile <(printf '1.3.6.1.4.1.57264.1.1=DER:%s' "$der_hex") 2>/dev/null
80+
rm -f "$CERT_DIR/machine-client.csr" "$CERT_DIR/machine-client-ca.srl"
81+
}
82+
83+
needs_certs() {
84+
[[ "$REGENERATE_CERTS" == "true" ]] && return 0
85+
for f in machine-client-ca.pem machine-client-ca-key.pem machine-client.pem machine-client-key.pem; do
86+
[[ -f "$CERT_DIR/$f" ]] || return 0
87+
done
88+
return 1
89+
}
90+
91+
if needs_certs; then
92+
generate_certs
93+
fi
94+
95+
echo "Append $CERT_DIR/machine-client-ca.pem to client_ca_path; restart gateway."
96+
read -r -p "Press Enter when done. " _
97+
98+
find_sidecar() {
99+
if [[ -n "$SIDECAR_BIN" ]]; then
100+
if [[ ! -x "$SIDECAR_BIN" ]]; then
101+
echo "error: sidecar binary not found or not executable: $SIDECAR_BIN" >&2
102+
exit 1
103+
fi
104+
printf '%s' "$SIDECAR_BIN"
105+
return
106+
fi
107+
108+
if command -v agent_gateway_sidecar &>/dev/null; then
109+
command -v agent_gateway_sidecar
110+
return
111+
fi
112+
113+
local candidates=(
114+
"./target/release/agent_gateway_sidecar"
115+
"./target/debug/agent_gateway_sidecar"
116+
)
117+
for candidate in "${candidates[@]}"; do
118+
if [[ -x "$candidate" ]]; then
119+
printf '%s' "$candidate"
120+
return
121+
fi
122+
done
123+
124+
echo "error: could not find agent_gateway_sidecar binary" >&2
125+
echo "hint: build with 'cargo build -p agent_gateway_sidecar' or pass --sidecar-bin" >&2
126+
exit 1
127+
}
128+
129+
SIDECAR="$(find_sidecar)"
130+
131+
SIDECAR_PID=""
132+
133+
cleanup() {
134+
if [[ -n "$SIDECAR_PID" ]]; then
135+
kill "$SIDECAR_PID" 2>/dev/null || true
136+
wait "$SIDECAR_PID" 2>/dev/null || true
137+
fi
138+
}
139+
140+
trap cleanup EXIT
141+
142+
"$SIDECAR" \
143+
--listen "$LISTEN" \
144+
--gateway "$GATEWAY" \
145+
--client-cert "$CERT_DIR/machine-client.pem" \
146+
--client-key "$CERT_DIR/machine-client-key.pem" \
147+
--ca-cert "$GATEWAY_CA" &
148+
SIDECAR_PID=$!
149+
150+
parse_listen_addr() {
151+
local addr="$1"
152+
if [[ "$addr" == \[* ]]; then
153+
LISTEN_HOST="${addr%%\]:*}"
154+
LISTEN_HOST="${LISTEN_HOST#\[}"
155+
LISTEN_PORT="${addr##*\]:}"
156+
else
157+
LISTEN_HOST="${addr%:*}"
158+
LISTEN_PORT="${addr##*:}"
159+
fi
160+
}
161+
162+
parse_listen_addr "$LISTEN"
163+
164+
echo "Waiting for sidecar on $LISTEN..."
165+
for _ in $(seq 1 50); do
166+
if (echo > "/dev/tcp/$LISTEN_HOST/$LISTEN_PORT") 2>/dev/null; then
167+
break
168+
fi
169+
if ! kill -0 "$SIDECAR_PID" 2>/dev/null; then
170+
echo "error: sidecar exited unexpectedly" >&2
171+
wait "$SIDECAR_PID" 2>/dev/null || true
172+
SIDECAR_PID=""
173+
exit 1
174+
fi
175+
sleep 0.1
176+
done
177+
178+
if ! (echo > "/dev/tcp/$LISTEN_HOST/$LISTEN_PORT") 2>/dev/null; then
179+
echo "error: sidecar did not become ready within 5 seconds" >&2
180+
exit 1
181+
fi
182+
183+
echo "Sidecar ready on $LISTEN"
184+
185+
PROXY_URL="http://$LISTEN"
186+
187+
HTTP_PROXY="$PROXY_URL" \
188+
HTTPS_PROXY="$PROXY_URL" \
189+
claude

examples/generate-certs.sh

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,27 @@
11
#!/usr/bin/env bash
2-
# Generate a self-signed CA, proxy server cert, and client cert for local
3-
# development. Writes PEM files to a certs/ directory (created if absent).
4-
#
5-
# Usage:
6-
# ./examples/generate-certs.sh # extension_value defaults to "agent-alpha"
7-
# ./examples/generate-certs.sh agent-beta # custom extension value
8-
#
9-
# The client cert contains a custom X.509 extension at OID 1.3.6.1.4.1.57264.1.1
10-
# with the given value encoded as a DER UTF8String.
2+
# Dev gateway server TLS only → certs/server-ca*.pem, certs/server*.pem. Client enrollment: examples/connect.sh.
113

124
set -euo pipefail
135

14-
EXT_VALUE="${1:-agent-alpha}"
156
DIR="certs"
167

178
mkdir -p "$DIR"
189

19-
der_utf8string() {
20-
local val="$1"
21-
local len=${#val}
22-
local result
23-
result=$(printf '0c:%02x' "$len")
24-
for (( i=0; i<len; i++ )); do
25-
result=$(printf '%s:%02x' "$result" "'${val:$i:1}")
26-
done
27-
printf '%s' "$result"
28-
}
29-
30-
echo "==> Generating CA"
10+
echo "==> Generating server CA"
3111
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
32-
-keyout "$DIR/client-ca-key.pem" -out "$DIR/client-ca.pem" \
33-
-days 365 -nodes -subj "/CN=agent-gateway CA" 2>/dev/null
12+
-keyout "$DIR/server-ca-key.pem" -out "$DIR/server-ca.pem" \
13+
-days 365 -nodes -subj "/CN=agent-gateway server CA" 2>/dev/null
3414

35-
echo "==> Generating proxy server cert (localhost / 127.0.0.1)"
15+
echo "==> Generating gateway server cert (localhost / 127.0.0.1)"
3616
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
37-
-keyout "$DIR/proxy-key.pem" -out "$DIR/proxy.csr" \
17+
-keyout "$DIR/server-key.pem" -out "$DIR/server.csr" \
3818
-nodes -subj "/CN=localhost" 2>/dev/null
39-
openssl x509 -req -in "$DIR/proxy.csr" \
40-
-CA "$DIR/client-ca.pem" -CAkey "$DIR/client-ca-key.pem" -CAcreateserial \
41-
-out "$DIR/proxy.pem" -days 365 \
19+
openssl x509 -req -in "$DIR/server.csr" \
20+
-CA "$DIR/server-ca.pem" -CAkey "$DIR/server-ca-key.pem" -CAcreateserial \
21+
-out "$DIR/server.pem" -days 365 \
4222
-extfile <(printf 'subjectAltName=DNS:localhost,IP:127.0.0.1') 2>/dev/null
43-
rm -f "$DIR/proxy.csr"
44-
45-
echo "==> Generating client cert (extension_value=$EXT_VALUE)"
46-
DER_HEX=$(der_utf8string "$EXT_VALUE")
47-
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
48-
-keyout "$DIR/client-key.pem" -out "$DIR/client.csr" \
49-
-nodes -subj "/CN=agent-client" 2>/dev/null
50-
openssl x509 -req -in "$DIR/client.csr" \
51-
-CA "$DIR/client-ca.pem" -CAkey "$DIR/client-ca-key.pem" -CAcreateserial \
52-
-out "$DIR/client.pem" -days 365 \
53-
-extfile <(printf '1.3.6.1.4.1.57264.1.1=DER:%s' "$DER_HEX") 2>/dev/null
54-
rm -f "$DIR/client.csr" "$DIR/client-ca.srl"
23+
rm -f "$DIR/server.csr" "$DIR/server-ca.srl"
5524

5625
echo ""
5726
echo "Generated in $DIR/:"
5827
ls -1 "$DIR"
59-
echo ""
60-
echo "Client extension: OID 1.3.6.1.4.1.57264.1.1 = \"$EXT_VALUE\""

examples/sample_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
python sample_client.py \\
1616
--proxy-host 127.0.0.1 \\
1717
--proxy-port 8443 \\
18-
--client-cert certs/client.pem \\
19-
--client-key certs/client-key.pem \\
20-
--ca-cert certs/proxy-ca.pem \\
18+
--client-cert ~/.local/share/agent-gateway/machine-client.pem \\
19+
--client-key ~/.local/share/agent-gateway/machine-client-key.pem \\
20+
--ca-cert certs/server-ca.pem \\
2121
--destination api.example.com:443 \\
2222
--dest-ca certs/dest-ca.pem # optional, uses system CAs if omitted
2323
"""

sidecar/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "agent_gateway_sidecar"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
tokio = { version = "1", features = ["full"] }
8+
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "logging", "std", "tls12"] }
9+
tokio-rustls = { version = "0.26", default-features = false, features = ["aws-lc-rs", "logging", "tls12"] }
10+
rustls-pemfile = "2"
11+
rustls-pki-types = "1"
12+
hyper = { version = "1", features = ["server", "client", "http1", "http2"] }
13+
hyper-util = { version = "0.1", features = ["tokio", "server-auto"] }
14+
http = "1"
15+
http-body-util = "0.1"
16+
bytes = "1"
17+
clap = { version = "4", features = ["derive"] }
18+
tracing = "0.1"
19+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
20+
anyhow = "1"

0 commit comments

Comments
 (0)