fix: unify strict spiffe identity validation
This commit is contained in:
parent
e318ddbb54
commit
57013dd6b2
@ -242,7 +242,8 @@ Gateway 不复用 `controlPlane.listen` 作为客户端地址。`listen` 是 Con
|
|||||||
|
|
||||||
当 `controlPlane.tls.mode=mtls` 时,Gateway 使用 `controlPlane.gatewayTLS` 中独立的
|
当 `controlPlane.tls.mode=mtls` 时,Gateway 使用 `controlPlane.gatewayTLS` 中独立的
|
||||||
客户端证书、私钥和 Controller CA 发起 TLS 1.3 连接;证书必须符合 Controller 的
|
客户端证书、私钥和 Controller CA 发起 TLS 1.3 连接;证书必须符合 Controller 的
|
||||||
SPIFFE Worker 身份校验。
|
SPIFFE Worker 身份校验。Gateway 自动解析和 Controller 授权共用严格 URI 规则:身份 URI
|
||||||
|
不得包含用户信息、端口、查询、片段或转义路径。
|
||||||
`disabled` 仅接受回环控制面地址,供本地 fixture 使用。
|
`disabled` 仅接受回环控制面地址,供本地 fixture 使用。
|
||||||
|
|
||||||
## 10. Checker 启动参数
|
## 10. Checker 启动参数
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import (
|
|||||||
|
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/peer"
|
"google.golang.org/grpc/peer"
|
||||||
|
|
||||||
|
"proxy-pool/internal/domain/workerruntime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrUnauthorizedIdentity = errors.New("control-plane identity is not authorized")
|
var ErrUnauthorizedIdentity = errors.New("control-plane identity is not authorized")
|
||||||
@ -57,22 +59,18 @@ func (authorizer *SPIFFEIdentityAuthorizer) authorize(ctx context.Context, resou
|
|||||||
return ErrUnauthorizedIdentity
|
return ErrUnauthorizedIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedPath := "/" + authorizer.environment + "/" + resourceType + "/" + resourceID
|
|
||||||
identityCount := 0
|
identityCount := 0
|
||||||
for _, chain := range tlsInfo.State.VerifiedChains {
|
for _, chain := range tlsInfo.State.VerifiedChains {
|
||||||
if len(chain) == 0 || chain[0] == nil {
|
if len(chain) == 0 || chain[0] == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, uri := range chain[0].URIs {
|
for _, uri := range chain[0].URIs {
|
||||||
if uri == nil || uri.Scheme != "spiffe" || uri.Host != authorizer.trustDomain {
|
identity, matches := workerruntime.SPIFFEIdentity(uri, authorizer.trustDomain, authorizer.environment, resourceType)
|
||||||
continue
|
if !matches {
|
||||||
}
|
|
||||||
segments := strings.Split(strings.Trim(uri.Path, "/"), "/")
|
|
||||||
if len(segments) != 3 || segments[1] != resourceType {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
identityCount++
|
identityCount++
|
||||||
if identityCount > 1 || uri.RawQuery != "" || uri.Fragment != "" || uri.Path != expectedPath {
|
if identityCount > 1 || identity != resourceID {
|
||||||
return ErrUnauthorizedIdentity
|
return ErrUnauthorizedIdentity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@ func TestSPIFFEIdentityAuthorizer(t *testing.T) {
|
|||||||
{name: "different worker", workerID: "worker-a", uri: "spiffe://proxy.example/prod/worker/worker-b", withTLS: true, wantError: true},
|
{name: "different worker", workerID: "worker-a", uri: "spiffe://proxy.example/prod/worker/worker-b", withTLS: true, wantError: true},
|
||||||
{name: "different environment", workerID: "worker-a", uri: "spiffe://proxy.example/staging/worker/worker-a", withTLS: true, wantError: true},
|
{name: "different environment", workerID: "worker-a", uri: "spiffe://proxy.example/staging/worker/worker-a", withTLS: true, wantError: true},
|
||||||
{name: "different trust domain", workerID: "worker-a", uri: "spiffe://other.example/prod/worker/worker-a", withTLS: true, wantError: true},
|
{name: "different trust domain", workerID: "worker-a", uri: "spiffe://other.example/prod/worker/worker-a", withTLS: true, wantError: true},
|
||||||
|
{name: "uri query is rejected", workerID: "worker-a", uri: "spiffe://proxy.example/prod/worker/worker-a?scope=ignored", withTLS: true, wantError: true},
|
||||||
|
{name: "uri user info is rejected", workerID: "worker-a", uri: "spiffe://worker@proxy.example/prod/worker/worker-a", withTLS: true, wantError: true},
|
||||||
{name: "missing peer tls", workerID: "worker-a", wantError: true},
|
{name: "missing peer tls", workerID: "worker-a", wantError: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"proxy-pool/internal/domain/workerruntime"
|
"proxy-pool/internal/domain/workerruntime"
|
||||||
)
|
)
|
||||||
@ -46,12 +45,5 @@ func ResolveSPIFFEIdentity(certificateFile, keyFile, trustDomain, environment, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func spiffeIdentity(uri *url.URL, trustDomain, environment, role string) (string, bool) {
|
func spiffeIdentity(uri *url.URL, trustDomain, environment, role string) (string, bool) {
|
||||||
if uri == nil || uri.Scheme != "spiffe" || uri.Host != trustDomain || uri.RawQuery != "" || uri.Fragment != "" {
|
return workerruntime.SPIFFEIdentity(uri, trustDomain, environment, role)
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
segments := strings.Split(strings.Trim(uri.Path, "/"), "/")
|
|
||||||
if len(segments) != 3 || segments[0] != environment || segments[1] != role || !workerruntime.ValidIdentifier(segments[2]) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return segments[2], true
|
|
||||||
}
|
}
|
||||||
|
|||||||
22
internal/domain/workerruntime/spiffe.go
Normal file
22
internal/domain/workerruntime/spiffe.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package workerruntime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SPIFFEIdentity returns the ID from an exact workload SPIFFE URI SAN. The
|
||||||
|
// control plane accepts no URI decorations so certificate parsing and server
|
||||||
|
// authorization use the same identity boundary.
|
||||||
|
func SPIFFEIdentity(uri *url.URL, trustDomain, environment, role string) (string, bool) {
|
||||||
|
if uri == nil || trustDomain == "" || environment == "" || (role != "worker" && role != "checker") ||
|
||||||
|
uri.Scheme != "spiffe" || uri.Host != trustDomain || uri.User != nil || uri.Port() != "" ||
|
||||||
|
uri.Opaque != "" || uri.RawPath != "" || uri.RawQuery != "" || uri.ForceQuery || uri.Fragment != "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
segments := strings.Split(uri.Path, "/")
|
||||||
|
if len(segments) != 4 || segments[0] != "" || segments[1] != environment || segments[2] != role || !ValidIdentifier(segments[3]) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return segments[3], true
|
||||||
|
}
|
||||||
@ -3,10 +3,39 @@ package workerruntime
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestSPIFFEIdentityAcceptsOnlyExactWorkloadURI(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "exact worker", raw: "spiffe://proxy.example/prod/worker/worker-a", want: "worker-a"},
|
||||||
|
{name: "checker is not worker", raw: "spiffe://proxy.example/prod/checker/checker-a"},
|
||||||
|
{name: "query", raw: "spiffe://proxy.example/prod/worker/worker-a?scope=ignored"},
|
||||||
|
{name: "user info", raw: "spiffe://worker@proxy.example/prod/worker/worker-a"},
|
||||||
|
{name: "port", raw: "spiffe://proxy.example:8443/prod/worker/worker-a"},
|
||||||
|
{name: "trailing slash", raw: "spiffe://proxy.example/prod/worker/worker-a/"},
|
||||||
|
{name: "escaped path", raw: "spiffe://proxy.example/prod/worker/worker%2Da"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
uri, err := url.Parse(test.raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, ok := SPIFFEIdentity(uri, "proxy.example", "prod", "worker")
|
||||||
|
if (test.want != "") != ok || got != test.want {
|
||||||
|
t.Fatalf("SPIFFEIdentity() = (%q, %t), want (%q, %t)", got, ok, test.want, test.want != "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeLabelsClonesAndBoundsValues(t *testing.T) {
|
func TestNormalizeLabelsClonesAndBoundsValues(t *testing.T) {
|
||||||
source := map[string]string{"region": "cn-north"}
|
source := map[string]string{"region": "cn-north"}
|
||||||
labels, err := NormalizeLabels(source)
|
labels, err := NormalizeLabels(source)
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
`PROXY_POOL_AUTO_IDENTITY=true` 时只接受与信任域、环境和角色精确匹配的唯一 SPIFFE URI,
|
`PROXY_POOL_AUTO_IDENTITY=true` 时只接受与信任域、环境和角色精确匹配的唯一 SPIFFE URI,
|
||||||
并在显式 ID 不匹配时拒绝启动。Compose 与 Kubernetes 开发 mTLS Overlay 已移除手工 ID,
|
并在显式 ID 不匹配时拒绝启动。Compose 与 Kubernetes 开发 mTLS Overlay 已移除手工 ID,
|
||||||
改由挂载证书派生;定向 Go、文档、Compose、Kustomize 与严格配置校验均通过。
|
改由挂载证书派生;定向 Go、文档、Compose、Kustomize 与严格配置校验均通过。
|
||||||
|
- SPIFFE URI 解析已下沉到 `workerruntime.SPIFFEIdentity`,Controller 授权和客户端
|
||||||
|
自动派生共用同一边界;用户信息、端口、查询、片段、转义路径和非精确角色路径均拒绝。
|
||||||
- Gateway 静态 `routing.action: direct` 已贯通严格配置、控制面协议、完整 Snapshot、
|
- Gateway 静态 `routing.action: direct` 已贯通严格配置、控制面协议、完整 Snapshot、
|
||||||
HTTP/CONNECT 转发和目标地址策略。直连请求不会申请 Proxy 容量、创建粘性绑定或上报
|
HTTP/CONNECT 转发和目标地址策略。直连请求不会申请 Proxy 容量、创建粘性绑定或上报
|
||||||
Proxy Outcome;`onUnavailable.action: direct` 仍保留为代理无候选时的独立回退语义。
|
Proxy Outcome;`onUnavailable.action: direct` 仍保留为代理无候选时的独立回退语义。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user