Relax the auth-sidecar proxy address policy so a remote central sidecar reachable over TLS can be used, while keeping existing same-host plaintext behavior unchanged. - ValidateProxyAddr: allow https:// to any host (cross-machine); http:// and bare host:port stay same-host only; userinfo/path/query/fragment remain rejected. - Add ProxyScheme and route the interceptor URL rewrite through the configured scheme (https for remote, http for same-host). ProxyScheme parses the address so a mixed-case HTTPS:// cannot silently downgrade to plaintext HTTP. - Update LARKSUITE_CLI_AUTH_PROXY doc and server-demo README for the new policy; refresh the package comment. - Tests: case-insensitive scheme, IPv6 https, https userinfo rejection, query/fragment rejection, ProxyHost https forms, and end-to-end interceptor scheme selection.
Sidecar Server Reference Implementation
⚠️ This is a demo. For production deployment, implement your own sidecar server conforming to the wire protocol in
github.com/larksuite/cli/sidecar.
This example shows how to implement a sidecar auth proxy server that receives HMAC-signed requests from lark-cli sandbox clients and forwards them to the Lark/Feishu API with real credentials injected.
What this demo shows
- HMAC-SHA256 request verification (timestamp drift, body digest, signature)
- Target host allowlist + https-only target validation (anti-SSRF / anti-downgrade)
- Identity-based token resolution (UAT for user, TAT for bot)
- Auth-header allowlist: real token may only be injected into
Authorization/X-Lark-MCP-UAT/X-Lark-MCP-TAT, rejecting attempts to smuggle it intoCookie,User-Agent, or other intermediate-logged headers - Audit logging with path ID-segment sanitization and upstream error truncation
- Safe request forwarding (strips client-supplied auth headers)
What this demo does NOT handle
- TAT refresh — the shared
DefaultTokenProvidercaches the TAT viasync.Once, which never refreshes. A long-running server will return an expired TAT after 2 hours. Production implementations should maintain a TTL-based cache with early renewal. - High availability / load balancing / hot key rotation
- TLS termination
- Rate limiting / per-identity quotas
Both sides need the right build tags
Sidecar is split into two separate binaries with different build tags:
| Side | Binary | Build tag | How to build |
|---|---|---|---|
| Sandbox (client) | lark-cli |
authsidecar |
go build -tags authsidecar -o lark-cli . |
| Trusted (server) | sidecar-server-demo |
authsidecar_demo |
go build -tags authsidecar_demo -o sidecar-server-demo ./sidecar/server-demo/ |
If the sandbox runs a standard lark-cli without -tags authsidecar, the
LARKSUITE_CLI_AUTH_PROXY env var is ignored and requests bypass the sidecar
entirely — real credentials (if any) leak to the sandbox.
Prerequisites
The demo reuses the lark-cli credential pipeline, so the trusted machine must have an app configured:
lark-cli config init --new # configure app_id / app_secret (required)
lark-cli auth login # store user refresh_token in keychain
# (only required if sandbox will use --as user)
auth login is only required for user identity. If the server will only
serve bot requests (TAT), config init alone is enough because the TAT is
minted from app_id + app_secret.
Also, the server process must not inherit LARKSUITE_CLI_AUTH_PROXY — if
it does, the sidecar credential provider would activate inside the server and
return sentinel tokens instead of real ones. The demo rejects this at startup
with a clear error, but you should make sure to unset LARKSUITE_CLI_AUTH_PROXY
in the server shell before launching.
Run
./sidecar-server-demo \
--listen 127.0.0.1:16384 \
--key-file <HOME>/.lark-sidecar/proxy.key \
--log-file <HOME>/.lark-sidecar/audit.log
Flags
| Flag | Default | Purpose |
|---|---|---|
--listen |
127.0.0.1:16384 |
Address to bind the HTTP listener |
--key-file |
<HOME>/.lark-sidecar/proxy.key |
Path to write the generated HMAC key (mode 0600) |
--log-file |
(empty, stderr) | Audit log output path |
--profile |
(empty, active profile) | lark-cli profile name for credential lookup |
Startup output
Auth sidecar listening on http://127.0.0.1:16384
HMAC key prefix: a3b2c1d4
Full key written to /Users/alice/.lark-sidecar/proxy.key (mode 0600)
Set in sandbox:
export LARKSUITE_CLI_AUTH_PROXY="http://127.0.0.1:16384"
export LARKSUITE_CLI_PROXY_KEY="<read from /Users/alice/.lark-sidecar/proxy.key>"
export LARKSUITE_CLI_APP_ID="cli_xxx"
export LARKSUITE_CLI_BRAND="feishu"
The key-file path is printed exactly as passed on the command line (relative
paths stay relative). The HMAC key prefix is the first 8 characters for
identification without revealing the full key.
Sandbox env vars (complete list)
The startup banner only prints the required variables. Two more are optional:
export LARKSUITE_CLI_AUTH_PROXY="http://..." # required (see constraints below)
export LARKSUITE_CLI_PROXY_KEY="..." # required
export LARKSUITE_CLI_APP_ID="cli_xxx" # required
export LARKSUITE_CLI_BRAND="feishu" # required (feishu | lark)
export LARKSUITE_CLI_DEFAULT_AS="user" # optional: force default identity
export LARKSUITE_CLI_STRICT_MODE="user" # optional: lock sandbox to one identity
LARKSUITE_CLI_AUTH_PROXY constraints — validated by the CLI on startup:
- Scheme must be
http:///https://(or barehost:port, treated as plaintext http).https://<any-host>is allowed, including a remote sidecar on another machine: TLS provides confidentiality over the network and the per-request HMAC signature provides integrity/authentication.- Plaintext
http://(and barehost:port) is allowed only same-host: loopback (127.0.0.1,::1) or a recognized same-host alias (localhost,host.docker.internal,host.containers.internal,host.lima.internal,gateway.docker.internal). For a remote sidecar, use anhttps://address.
- No path, query, fragment, or
user:pass@in the URL.
Note: this demo server itself terminates plain HTTP and is meant to run locally. A production remote sidecar must terminate TLS (its own
https://endpoint, e.g. behind a load balancer or with a real certificate); the CLI-side policy above is what enables pointing at it.
How auto identity detection works in sidecar mode: on every invocation the
CLI asks the sidecar to look up the logged-in user's open_id via
/open-apis/authen/v1/user_info. If that succeeds, --as defaults to user;
if it fails (trusted side has no valid user login, or the call errors out),
it falls back to bot. Setting LARKSUITE_CLI_DEFAULT_AS=user lets you
short-circuit this and always default to user regardless of the lookup
result; set it to bot for the opposite.
Note: LARKSUITE_CLI_STRICT_MODE and the server's identity allowlist are
two separate enforcement points:
STRICT_MODEis interpreted locally by the sandbox CLI — it rejects--asvalues the sandbox itself disallows, before any request goes out.- The server's allowlist is built from the trusted-side config's
SupportedIdentities(sidecar/server-demo/allowlist.go). The sandbox cannot override it.
A well-configured deployment aligns both (e.g. both set to user when the
app only supports user tokens), but they are computed independently.
Graceful shutdown
Send SIGINT (Ctrl+C) or SIGTERM to stop the server. The demo drains
in-flight requests with a 5-second timeout before exiting.
Wire protocol
See the sidecar package on pkg.go.dev
for protocol constants, HMAC signing/verification, and address validation utilities.
Headers (client → server):
| Header | Purpose |
|---|---|
X-Lark-Proxy-Version |
Wire-protocol version (currently "v1"). Server rejects unknown values with 400. |
X-Lark-Proxy-Target |
Original target scheme + host only (e.g. https://open.feishu.cn). Must be https://; any path/query/fragment/userinfo in this header is rejected. The path and query come from the request line itself; the server reconstructs the upstream URL as https://<host> + requestURI. |
X-Lark-Proxy-Identity |
"user" or "bot". Covered by the signature. |
X-Lark-Proxy-Auth-Header |
Which header the server should inject real token into. Covered by the signature. |
X-Lark-Proxy-Signature |
hex-encoded HMAC-SHA256 |
X-Lark-Proxy-Timestamp |
Unix seconds (drift ≤ 60s) |
X-Lark-Body-SHA256 |
hex-encoded SHA-256 of the request body |
Signing material (newline-separated, in order):
version
method
host
pathAndQuery
bodySHA256
timestamp
identity
authHeader
Every field above is part of the canonical string. In particular, identity
and authHeader are covered so a captured request cannot be replayed with
its identity flipped (bot↔user) or its auth-header redirected (e.g. into
Cookie) inside the 60s drift window.
Source layout
| File | Purpose |
|---|---|
main.go |
Entry point: flag parsing, server lifecycle |
handler.go |
proxyHandler.ServeHTTP — main request flow |
forward.go |
Forwarding HTTP client + proxy-header filter |
allowlist.go |
Target host / identity allowlists |
audit.go |
Log path/error sanitization |
handler_test.go |
Unit tests for all of the above |
See also
- server-multi-tenant-demo — extends this demo with per-client HMAC key isolation, OAuth device-flow login, and persistent client → user mapping for multi-tenant deployments