Compare commits

...

2 Commits

Author SHA1 Message Date
shanglei
52dc09af95 style(sidecar): gofmt hmac_test.go
Align comment spacing flagged by the fast-gate gofmt check.
2026-06-02 20:16:42 +08:00
shanglei
07da0c8090 feat(sidecar): support remote HTTPS sidecar addresses
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.
2026-06-02 20:13:47 +08:00
6 changed files with 247 additions and 67 deletions

View File

@@ -4,11 +4,12 @@
//go:build authsidecar //go:build authsidecar
// Package sidecar provides a transport interceptor for the auth sidecar // Package sidecar provides a transport interceptor for the auth sidecar
// proxy mode. When LARKSUITE_CLI_AUTH_PROXY is set (an HTTP URL), all // proxy mode. When LARKSUITE_CLI_AUTH_PROXY is set (an http:// or https://
// outgoing requests are rewritten to the sidecar address. The interceptor // URL), all outgoing requests are rewritten to the sidecar address. The
// strips placeholder credentials, injects proxy headers, and signs each // interceptor strips placeholder credentials, injects proxy headers, and
// request with HMAC-SHA256. No custom DialContext is needed — Go's // signs each request with HMAC-SHA256. No custom DialContext is needed —
// standard http.Transport connects to the sidecar via plain HTTP. // Go's standard http.Transport connects to the sidecar via HTTP, or via
// HTTPS (TLS) when the sidecar address is an https:// URL.
package sidecar package sidecar
import ( import (
@@ -48,13 +49,15 @@ func (p *Provider) ResolveInterceptor(ctx context.Context) transport.Interceptor
return &Interceptor{ return &Interceptor{
key: []byte(key), key: []byte(key),
sidecarHost: sidecar.ProxyHost(proxyAddr), sidecarHost: sidecar.ProxyHost(proxyAddr),
sidecarScheme: sidecar.ProxyScheme(proxyAddr),
} }
} }
// Interceptor rewrites requests for the sidecar proxy. // Interceptor rewrites requests for the sidecar proxy.
type Interceptor struct { type Interceptor struct {
key []byte // HMAC signing key key []byte // HMAC signing key
sidecarHost string // sidecar host:port for URL rewriting sidecarHost string // sidecar host[:port] for URL rewriting
sidecarScheme string // "http" (same-host) or "https" (remote TLS sidecar)
} }
// PreRoundTrip rewrites the request for sidecar routing when it carries a // PreRoundTrip rewrites the request for sidecar routing when it carries a
@@ -130,8 +133,13 @@ func (i *Interceptor) PreRoundTrip(req *http.Request) func(resp *http.Response,
req.Header.Set(sidecar.HeaderProxyTimestamp, ts) req.Header.Set(sidecar.HeaderProxyTimestamp, ts)
req.Header.Set(sidecar.HeaderProxySignature, sig) req.Header.Set(sidecar.HeaderProxySignature, sig)
// 5. Rewrite URL to route through sidecar // 5. Rewrite URL to route through sidecar. Scheme follows the configured
req.URL.Scheme = "http" // proxy address: https for a remote (TLS) sidecar, http for a same-host one.
scheme := i.sidecarScheme
if scheme == "" {
scheme = "http"
}
req.URL.Scheme = scheme
req.URL.Host = i.sidecarHost req.URL.Host = i.sidecarHost
return nil // no post-hook needed return nil // no post-hook needed

View File

@@ -7,11 +7,13 @@ package sidecar
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"io" "io"
"net/http" "net/http"
"testing" "testing"
"github.com/larksuite/cli/internal/envvars"
"github.com/larksuite/cli/sidecar" "github.com/larksuite/cli/sidecar"
) )
@@ -97,6 +99,54 @@ func TestInterceptor_PreRoundTrip(t *testing.T) {
} }
} }
// TestInterceptor_PreRoundTrip_HTTPS verifies that a remote (TLS) sidecar
// rewrites the request to https://<remote-host>, while still preserving the
// original target and signing the request.
func TestInterceptor_PreRoundTrip_HTTPS(t *testing.T) {
key := []byte("test-key-for-hmac-signing-32byte!")
interceptor := &Interceptor{key: key, sidecarHost: "sidecar.mycorp.com", sidecarScheme: "https"}
req, _ := http.NewRequest("GET", "https://open.feishu.cn/open-apis/im/v1/chats", nil)
req.Header.Set("Authorization", "Bearer "+sidecar.SentinelUAT)
interceptor.PreRoundTrip(req)
if req.URL.Scheme != "https" {
t.Errorf("scheme = %q, want %q", req.URL.Scheme, "https")
}
if req.URL.Host != "sidecar.mycorp.com" {
t.Errorf("host = %q, want %q", req.URL.Host, "sidecar.mycorp.com")
}
// Original target still preserved for the sidecar to forward upstream.
if target := req.Header.Get(sidecar.HeaderProxyTarget); target != "https://open.feishu.cn" {
t.Errorf("target = %q, want %q", target, "https://open.feishu.cn")
}
// Request is still signed.
if sig := req.Header.Get(sidecar.HeaderProxySignature); sig == "" {
t.Error("signature header should be set")
}
}
// TestResolveInterceptor_HTTPSScheme pins the end-to-end env→scheme path: a
// (mixed-case) https proxy address must produce an interceptor that rewrites to
// https, never silently downgrading a remote sidecar to plaintext http.
func TestResolveInterceptor_HTTPSScheme(t *testing.T) {
t.Setenv(envvars.CliAuthProxy, "HTTPS://sidecar.mycorp.com") // uppercase on purpose
t.Setenv(envvars.CliProxyKey, "key")
ic := (&Provider{}).ResolveInterceptor(context.Background())
si, ok := ic.(*Interceptor)
if !ok || si == nil {
t.Fatalf("expected *Interceptor, got %T", ic)
}
if si.sidecarScheme != "https" {
t.Errorf("sidecarScheme = %q, want %q (uppercase HTTPS must not downgrade)", si.sidecarScheme, "https")
}
if si.sidecarHost != "sidecar.mycorp.com" {
t.Errorf("sidecarHost = %q, want %q", si.sidecarHost, "sidecar.mycorp.com")
}
}
func TestInterceptor_BotIdentity(t *testing.T) { func TestInterceptor_BotIdentity(t *testing.T) {
interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"} interceptor := &Interceptor{key: []byte("key"), sidecarHost: "127.0.0.1:16384"}

View File

@@ -13,7 +13,7 @@ const (
CliStrictMode = "LARKSUITE_CLI_STRICT_MODE" CliStrictMode = "LARKSUITE_CLI_STRICT_MODE"
// Sidecar proxy (auth proxy mode) // Sidecar proxy (auth proxy mode)
CliAuthProxy = "LARKSUITE_CLI_AUTH_PROXY" // sidecar HTTP address, e.g. "http://127.0.0.1:16384" CliAuthProxy = "LARKSUITE_CLI_AUTH_PROXY" // sidecar address http(s)://host[:port]; plaintext http is same-host only, a remote sidecar must use https. e.g. "http://127.0.0.1:16384" or "https://sidecar.mycorp.com"
CliProxyKey = "LARKSUITE_CLI_PROXY_KEY" // HMAC signing key shared with sidecar CliProxyKey = "LARKSUITE_CLI_PROXY_KEY" // HMAC signing key shared with sidecar
// Content safety scanning mode // Content safety scanning mode

View File

@@ -161,6 +161,11 @@ func TestValidateProxyAddr(t *testing.T) {
"http://gateway.docker.internal:16384", "http://gateway.docker.internal:16384",
// trailing slash is tolerated // trailing slash is tolerated
"http://127.0.0.1:8080/", "http://127.0.0.1:8080/",
// https: any valid host (including remote, cross-machine) is allowed
"https://127.0.0.1:16384",
"https://sidecar.mycorp.com",
"https://sidecar.mycorp.com:8443",
"https://sidecar.corp.internal:443/",
} }
for _, addr := range valid { for _, addr := range valid {
if err := ValidateProxyAddr(addr); err != nil { if err := ValidateProxyAddr(addr); err != nil {
@@ -242,6 +247,8 @@ func TestValidateProxyAddr_RejectsUserinfo(t *testing.T) {
"http://user@127.0.0.1:16384", "http://user@127.0.0.1:16384",
"http://user:pass@127.0.0.1:16384", "http://user:pass@127.0.0.1:16384",
"http://127.0.0.1@attacker.com:16384", "http://127.0.0.1@attacker.com:16384",
"https://x@evil.com",
"https://user:pass@sidecar.mycorp.com",
} { } {
err := ValidateProxyAddr(addr) err := ValidateProxyAddr(addr)
if err == nil { if err == nil {
@@ -259,23 +266,99 @@ func TestValidateProxyAddr_RejectsUserinfo(t *testing.T) {
} }
} }
// TestValidateProxyAddr_HTTPSRejected pins the current contract: https is // TestValidateProxyAddr_HTTPSAllowed pins the contract: https addresses are
// rejected explicitly (not lumped into a generic "bad scheme" error) because // accepted, including a remote sidecar on another machine. TLS provides
// the interceptor hardcodes http and would silently downgrade an https URL // confidentiality over the network and the HMAC signature provides
// otherwise. The message must mention https so users understand why their // integrity/auth, so cross-machine https is supported.
// perfectly-looking config is refused. func TestValidateProxyAddr_HTTPSAllowed(t *testing.T) {
func TestValidateProxyAddr_HTTPSRejected(t *testing.T) {
for _, addr := range []string{ for _, addr := range []string{
"https://127.0.0.1:16384", "https://127.0.0.1:16384", // same-host over TLS
"https://sidecar.corp.internal:443", "https://sidecar.corp.internal:443",
"https://sidecar.mycorp.com", // remote, no explicit port
"https://sidecar.mycorp.com:8443",
} {
if err := ValidateProxyAddr(addr); err != nil {
t.Errorf("ValidateProxyAddr(%q): expected accepted, got: %v", addr, err)
}
}
}
// TestValidateProxyAddr_HTTPRemoteRejected: plaintext http to a non-same-host
// address stays rejected — a remote sidecar must use https.
func TestValidateProxyAddr_HTTPRemoteRejected(t *testing.T) {
for _, addr := range []string{
"http://sidecar.mycorp.com",
"http://sidecar.mycorp.com:8080",
"http://10.0.0.1:16384",
} { } {
err := ValidateProxyAddr(addr) err := ValidateProxyAddr(addr)
if err == nil { if err == nil {
t.Errorf("ValidateProxyAddr(%q): expected error, got nil", addr) t.Errorf("ValidateProxyAddr(%q): expected rejection (http remote), got nil", addr)
continue continue
} }
if !strings.Contains(err.Error(), "https") { msg := err.Error()
t.Errorf("ValidateProxyAddr(%q): error should mention https, got: %v", addr, err) if !strings.Contains(msg, "https") && !strings.Contains(msg, "same-host") && !strings.Contains(msg, "loopback") {
t.Errorf("ValidateProxyAddr(%q): error should point to https/same-host, got: %v", addr, err)
}
}
}
// TestProxyScheme: scheme is https only for https:// addresses, http otherwise.
// Case-insensitive: HTTPS:// must resolve to https, otherwise a remote sidecar
// would silently downgrade to plaintext http (see ProxyScheme doc).
func TestProxyScheme(t *testing.T) {
tests := map[string]string{
"https://sidecar.mycorp.com": "https",
"https://127.0.0.1:16384": "https",
"http://127.0.0.1:16384": "http",
"127.0.0.1:16384": "http",
// case-insensitive scheme
"HTTPS://sidecar.mycorp.com": "https",
"Https://sidecar.mycorp.com": "https",
"HtTp://127.0.0.1:16384": "http",
}
for in, want := range tests {
if got := ProxyScheme(in); got != want {
t.Errorf("ProxyScheme(%q) = %q, want %q", in, got, want)
}
}
}
// TestValidateProxyAddr_SchemeCaseInsensitive: mixed-case scheme must follow the
// same policy as lower-case — HTTPS accepted (remote allowed), HTTP remote
// rejected — so case can't be used to bypass the plaintext same-host rule.
func TestValidateProxyAddr_SchemeCaseInsensitive(t *testing.T) {
for _, addr := range []string{"HTTPS://sidecar.mycorp.com", "Https://sidecar.corp.internal:443"} {
if err := ValidateProxyAddr(addr); err != nil {
t.Errorf("ValidateProxyAddr(%q): expected accepted, got: %v", addr, err)
}
}
for _, addr := range []string{"HtTp://sidecar.mycorp.com", "HTTP://10.0.0.1:16384"} {
if err := ValidateProxyAddr(addr); err == nil {
t.Errorf("ValidateProxyAddr(%q): expected rejection (http remote), got nil", addr)
}
}
}
// TestValidateProxyAddr_IPv6HTTPS pins IPv6 https forms.
func TestValidateProxyAddr_IPv6HTTPS(t *testing.T) {
for _, addr := range []string{"https://[::1]:443", "https://[::1]"} {
if err := ValidateProxyAddr(addr); err != nil {
t.Errorf("ValidateProxyAddr(%q): expected accepted, got: %v", addr, err)
}
}
}
// TestValidateProxyAddr_RejectsQueryFragment: a proxy address must not carry a
// query or fragment, for either scheme.
func TestValidateProxyAddr_RejectsQueryFragment(t *testing.T) {
for _, addr := range []string{
"https://sidecar.mycorp.com?x=1",
"https://sidecar.mycorp.com#frag",
"http://127.0.0.1:16384?x=1",
} {
if err := ValidateProxyAddr(addr); err == nil {
t.Errorf("ValidateProxyAddr(%q): expected rejection, got nil", addr)
} }
} }
} }
@@ -289,6 +372,10 @@ func TestProxyHost(t *testing.T) {
{"http://0.0.0.0:8080", "0.0.0.0:8080"}, {"http://0.0.0.0:8080", "0.0.0.0:8080"},
{"http://host.docker.internal:16384/", "host.docker.internal:16384"}, {"http://host.docker.internal:16384/", "host.docker.internal:16384"},
{"127.0.0.1:16384", "127.0.0.1:16384"}, // no scheme {"127.0.0.1:16384", "127.0.0.1:16384"}, // no scheme
// https forms (remote sidecar)
{"https://sidecar.mycorp.com", "sidecar.mycorp.com"},
{"https://sidecar.mycorp.com:8443/", "sidecar.mycorp.com:8443"},
{"HTTPS://sidecar.mycorp.com", "sidecar.mycorp.com"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) { t.Run(tt.input, func(t *testing.T) {

View File

@@ -3,7 +3,8 @@
// Package sidecar defines the wire protocol shared between the CLI client // Package sidecar defines the wire protocol shared between the CLI client
// (running inside a sandbox) and the auth sidecar proxy (running in a // (running inside a sandbox) and the auth sidecar proxy (running in a
// trusted environment). Communication uses plain HTTP. // trusted environment). Communication uses HTTP for a same-host sidecar, or
// HTTPS (TLS) for a remote sidecar.
package sidecar package sidecar
import ( import (
@@ -103,32 +104,31 @@ func isSameHost(host string) bool {
return false return false
} }
// errNotSameHost is the shared error returned when the sidecar address does // errNotSameHost is the shared error returned when a plaintext (http) sidecar
// not resolve to the same physical host as the sandbox. Kept in one place so // address does not resolve to the same physical host as the sandbox. Kept in
// tests can look for a stable marker. // one place so tests can look for a stable marker.
func errNotSameHost(addr string) error { func errNotSameHost(addr string) error {
return fmt.Errorf("invalid proxy address %q: host must be loopback "+ return fmt.Errorf("invalid proxy address %q: a plaintext (http) sidecar must be "+
"(127.0.0.1 / ::1) or a recognized same-host alias "+ "loopback (127.0.0.1 / ::1) or a recognized same-host alias "+
"(localhost, host.docker.internal, host.containers.internal, "+ "(localhost, host.docker.internal, host.containers.internal, "+
"host.lima.internal, gateway.docker.internal). "+ "host.lima.internal, gateway.docker.internal). "+
"The sidecar must run on the same physical machine as the sandbox — "+ "For a remote sidecar on another machine, use an https:// address instead", addr)
"cross-machine deployment is not a sidecar and is not supported", addr)
} }
// ValidateProxyAddr validates the LARKSUITE_CLI_AUTH_PROXY value. // ValidateProxyAddr validates the LARKSUITE_CLI_AUTH_PROXY value.
// Accepted formats: // Accepted formats:
// - http://host:port // - https://host[:port] (remote sidecar; cross-machine allowed)
// - host:port (bare address, treated as http) // - http://host:port (plaintext; same-host only)
// - host:port (bare address, treated as plaintext http; same-host only)
// //
// Host must be loopback or in sameHostAliases. The sidecar pattern is // Scheme policy:
// inherently same-machine; cross-machine deployment is a different product // - https:// — any valid host is allowed, including a remote central sidecar
// and is not supported by this feature. // on another machine. TLS provides confidentiality over the untrusted
// // network; the per-request HMAC signature provides integrity/auth.
// https:// is rejected because sidecar is a same-host pattern: loopback // - http:// (or bare host:port) — plaintext, allowed only when the host is
// and virtual same-host bridges don't traverse any untrusted medium, so // loopback (127.0.0.1 / ::1) or a recognized same-host alias (a virtual
// TLS adds no security. Cross-machine deployment is out of scope (see the // same-host bridge that stays on the physical machine). For a remote
// host constraint above), so there is no scenario today where https // sidecar, use an https:// address instead.
// provides a real benefit over http on loopback.
// //
// userinfo (user:pass@) is rejected unconditionally — the sidecar protocol // userinfo (user:pass@) is rejected unconditionally — the sidecar protocol
// does not use basic auth, and the syntactic slot exists only as a phishing // does not use basic auth, and the syntactic slot exists only as a phishing
@@ -140,11 +140,11 @@ func ValidateProxyAddr(addr string) error {
return fmt.Errorf("proxy address is empty") return fmt.Errorf("proxy address is empty")
} }
// Bare host:port (no scheme) — validate as a net address. // Bare host:port (no scheme) — treated as plaintext http, so same-host only.
if !strings.Contains(addr, "://") { if !strings.Contains(addr, "://") {
host, port, err := net.SplitHostPort(addr) host, port, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
return fmt.Errorf("invalid proxy address %q: expected host:port or http://host:port", addr) return fmt.Errorf("invalid proxy address %q: expected host:port or http(s)://host[:port]", addr)
} }
if host == "" || port == "" { if host == "" || port == "" {
return fmt.Errorf("invalid proxy address %q: host and port must not be empty", addr) return fmt.Errorf("invalid proxy address %q: host and port must not be empty", addr)
@@ -159,33 +159,47 @@ func ValidateProxyAddr(addr string) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid proxy address %q: %w", addr, err) return fmt.Errorf("invalid proxy address %q: %w", addr, err)
} }
// userinfo (user:pass@) is rejected unconditionally (phishing vector).
if u.User != nil { if u.User != nil {
return fmt.Errorf("invalid proxy address %q: userinfo is not allowed", addr) return fmt.Errorf("invalid proxy address %q: userinfo is not allowed", addr)
} }
if u.Scheme == "https" {
return fmt.Errorf("invalid proxy address %q: use http:// — sidecar is "+
"same-host only (loopback or virtual same-host bridge), so TLS adds "+
"no security; cross-machine deployment is out of scope", addr)
}
if u.Scheme != "http" {
return fmt.Errorf("invalid proxy address %q: scheme must be http", addr)
}
if u.Host == "" { if u.Host == "" {
return fmt.Errorf("invalid proxy address %q: missing host", addr) return fmt.Errorf("invalid proxy address %q: missing host", addr)
} }
if u.Path != "" && u.Path != "/" { if u.Path != "" && u.Path != "/" {
return fmt.Errorf("invalid proxy address %q: path is not allowed", addr) return fmt.Errorf("invalid proxy address %q: path is not allowed", addr)
} }
if u.RawQuery != "" {
return fmt.Errorf("invalid proxy address %q: query is not allowed", addr)
}
if u.Fragment != "" {
return fmt.Errorf("invalid proxy address %q: fragment is not allowed", addr)
}
switch u.Scheme {
case "https":
// Remote sidecar over TLS. Cross-machine is allowed: https provides
// confidentiality over the network and the per-request HMAC signature
// provides integrity/authentication, so a remote central sidecar is
// supported without exposing credentials or signing material in clear.
return nil
case "http":
// Plaintext: only safe on the same physical host (loopback or a virtual
// same-host bridge). For a remote sidecar use an https:// address.
// u.Hostname() strips the port and unwraps IPv6 brackets. // u.Hostname() strips the port and unwraps IPv6 brackets.
if !isSameHost(u.Hostname()) { if !isSameHost(u.Hostname()) {
return errNotSameHost(addr) return errNotSameHost(addr)
} }
return nil return nil
default:
return fmt.Errorf("invalid proxy address %q: scheme must be http or https", addr)
}
} }
// ProxyHost extracts the host:port from an AUTH_PROXY URL. // ProxyHost extracts the host:port from an AUTH_PROXY URL.
// Input is expected to be an HTTP URL like "http://127.0.0.1:16384". // Input is expected to be an http:// or https:// URL like
// Returns the host:port portion for URL rewriting. // "http://127.0.0.1:16384" or "https://sidecar.mycorp.com".
// Returns the host[:port] portion for URL rewriting.
func ProxyHost(authProxy string) string { func ProxyHost(authProxy string) string {
// Strip scheme // Strip scheme
host := authProxy host := authProxy
@@ -196,3 +210,19 @@ func ProxyHost(authProxy string) string {
host = strings.TrimRight(host, "/") host = strings.TrimRight(host, "/")
return host return host
} }
// ProxyScheme returns the URL scheme the CLI must use when routing to the
// sidecar: "https" for a TLS (remote) sidecar, otherwise "http" (same-host
// plaintext). Input is a value already accepted by ValidateProxyAddr.
//
// It parses the address (rather than a case-sensitive prefix check) so the
// result stays consistent with ValidateProxyAddr, which relies on url.Parse
// normalizing the scheme. Otherwise "HTTPS://host" — accepted as https by
// ValidateProxyAddr — would silently downgrade to plaintext http here,
// breaking the "remote must use TLS" boundary.
func ProxyScheme(authProxy string) string {
if u, err := url.Parse(authProxy); err == nil && strings.EqualFold(u.Scheme, "https") {
return "https"
}
return "http"
}

View File

@@ -114,18 +114,23 @@ export LARKSUITE_CLI_STRICT_MODE="user" # optional: lock sandbox to o
**`LARKSUITE_CLI_AUTH_PROXY` constraints** — validated by the CLI on startup: **`LARKSUITE_CLI_AUTH_PROXY` constraints** — validated by the CLI on startup:
- Scheme must be `http://` (or bare `host:port`). `https://` is rejected - Scheme must be `http://` / `https://` (or bare `host:port`, treated as
today because the interceptor does not yet perform TLS; a future PR that plaintext http).
wires up real TLS will relax this. - `https://<any-host>` is allowed, **including a remote sidecar on another
- Host must be loopback (`127.0.0.1`, `::1`) or one of the recognized machine**: TLS provides confidentiality over the network and the
same-host aliases: `localhost`, `host.docker.internal`, per-request HMAC signature provides integrity/authentication.
`host.containers.internal`, `host.lima.internal`, `gateway.docker.internal`. - Plaintext `http://` (and bare `host:port`) is allowed **only same-host**:
The sidecar pattern is inherently same-machine; cross-machine deployment loopback (`127.0.0.1`, `::1`) or a recognized same-host alias
is a different product (auth broker / STS) with different security (`localhost`, `host.docker.internal`, `host.containers.internal`,
requirements (mTLS, cert rotation, per-client keys) and is not supported `host.lima.internal`, `gateway.docker.internal`). For a remote sidecar,
by this feature. use an `https://` address.
- No path, query, fragment, or `user:pass@` in the URL. - 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 **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 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`; `/open-apis/authen/v1/user_info`. If that succeeds, `--as` defaults to `user`;