Files
seaweedfs/weed/security/tls_client_cert_test.go
T
Chris LuandDevin e98adfd3bb security: fail closed on gRPC TLS errors, verify dialed server identity (#12263)
* security: fail closed on misconfigured gRPC TLS

LoadServerTLS/LoadClientTLS returned plaintext credentials on every
error path once TLS was configured: a typo in tls.min_version or
tls.cipher_suites, cert and key in different directories, or cert/key
set without grpc.ca all left the component serving or dialing
plaintext. A partially configured TLS setup now stops the process at
startup instead; leaving cert/key unset still means no TLS, as before.

The loaders also stat cert/key/ca files up front since the pemfile
providers read lazily and would otherwise only fail at handshake time.

Mirrored in the Rust volume server: partial grpc.volume cert/key, a
missing grpc.ca, or a rustls build failure now abort startup rather
than warn-and-continue on plaintext.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* security: optional server-identity allowlist for gRPC clients

The mTLS client accepts any certificate the configured CA signs, so a
per-workload CA lets one component stand in for any other: a filer
identity can answer as the master. The server side already screens
callers via allowed_commonNames and allowed_wildcard_domain; nothing
screened the server.

LoadClientTLS now wires AdditionalPeerVerification when the caller
configures a server allowlist: grpc.<component>.allowed_server_commonNames
(falling back to a shared grpc.allowed_server_commonNames) matched
against the peer cert's subject CN, DNS SANs, or URI SANs such as a
SPIFFE id, plus a grpc.allowed_server_wildcard_domain suffix match.
Leaving them unset keeps the previous behavior.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* security: URI SANs are exact-match only in the server identity allowlist

A URI identity whose path happens to end in the wildcard domain would
otherwise pass the DNS suffix check.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* security: tighten gRPC TLS fail-closed and server-identity paths

- URI SANs match the server-identity allowlist exactly; the wildcard
  domain suffix check applies only to CN and DNS SANs.
- A partially set client_cert/client_key pair is an error when the
  cert/key fallback is empty, instead of dialing plaintext.
- Cert/key/ca files are loaded and parsed at startup (LoadX509KeyPair
  and CA PEM validation) since pemfile providers read lazily.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rust volume: error on a partial grpc client_cert/client_key pair

Mirror the Go loadClientTLS path: when client_cert or client_key is
set without its mate and the cert/key fallback is empty, fail instead
of dialing plaintext.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* security: wildcard domain match requires a valid hostname

A CA-signed identity like ".example.com" satisfied the bare suffix check
for wildcard ".example.com" despite not being a hostname. Require a real
subdomain: non-empty label before the suffix, a '.' boundary when the
configured wildcard has no leading dot, and DNS-name syntax. A leading
"*." label still matches so wildcard certs keep working. Applied to both
the Go server/client authenticators and the Rust volume's client-cert
verifier.

* security: ignore a trailing root dot in wildcard domain matching

* tls: reject a partial dedicated client cert/key pair instead of falling back

When exactly one of component.client_cert/client_key is set, warn-and-
fallback silently dialed with the serving identity. Fail closed like the
other TLS misconfigurations in this change; mirrored in the Rust loader.
Regression tests cover a partial pair with a valid fallback.

* rust volume: harden partial client cert pair test with real pem files

* security: verify dialed server identity per target kind on the filer

The filer reused one grpc.filer-verifying dial option for master, volume,
filer and s3 targets, so a broad allowlist accepted the wrong component at
the wrong port and a narrow one rejected legitimate peers. Outbound calls
now pick grpc.<target>.allowed_server_commonNames (falling back to the
client component's and the global list) while still presenting the
grpc.filer client identity.

* security: scope per-target server allowlists under the client component

The filer read grpc.volume.allowed_server_commonNames when dialing a
volume, but that key already denotes the volume's own outbound allowlist
(e.g. the master identity). A deployment restricting volume->master
calls would make the filer reject real volume servers. Per-target policy
now lives under the client component: grpc.filer.volume_*
falls back to grpc.filer.* then grpc.*.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-30 20:04:45 -07:00

534 lines
19 KiB
Go

package security
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"github.com/spf13/viper"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
type testCA struct {
cert *x509.Certificate
key *ecdsa.PrivateKey
file string
}
func newTestCA(t *testing.T, dir string) *testCA {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test-ca"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
cert, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
file := filepath.Join(dir, "ca.pem")
writePem(t, file, "CERTIFICATE", der)
return &testCA{cert: cert, key: key, file: file}
}
// issue creates a leaf certificate restricted to the given extended key usages.
func (ca *testCA) issue(t *testing.T, dir, name string, ekus []x509.ExtKeyUsage) (certFile, keyFile string) {
return ca.issueWithSANs(t, dir, name, ekus, []string{"localhost"}, nil)
}
func (ca *testCA) issueWithSANs(t *testing.T, dir, name string, ekus []x509.ExtKeyUsage, dnsNames []string, uris []string) (certFile, keyFile string) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: name},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: ekus,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: dnsNames,
}
for _, u := range uris {
parsed, err := url.Parse(u)
if err != nil {
t.Fatal(err)
}
template.URIs = append(template.URIs, parsed)
}
der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key)
if err != nil {
t.Fatal(err)
}
keyDer, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
certFile = filepath.Join(dir, name+".pem")
keyFile = filepath.Join(dir, name+".key")
writePem(t, certFile, "CERTIFICATE", der)
writePem(t, keyFile, "EC PRIVATE KEY", keyDer)
return certFile, keyFile
}
func writePem(t *testing.T, file, blockType string, der []byte) {
t.Helper()
if err := os.WriteFile(file, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: der}), 0600); err != nil {
t.Fatal(err)
}
}
func startTestGrpcServer(t *testing.T, config *util.ViperProxy, component string) string {
t.Helper()
creds, _ := LoadServerTLS(config, component)
if creds == nil {
t.Fatal("LoadServerTLS returned nil")
}
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
server := grpc.NewServer(creds)
healthpb.RegisterHealthServer(server, health.NewServer())
go server.Serve(lis)
t.Cleanup(server.Stop)
return lis.Addr().String()
}
func healthCheck(t *testing.T, addr string, dialOption grpc.DialOption) error {
t.Helper()
conn, err := grpc.NewClient(addr, dialOption)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = healthpb.NewHealthClient(conn).Check(ctx, &healthpb.HealthCheckRequest{})
return err
}
// A component configured with a serverAuth-only serving cert plus a
// clientAuth-only client_cert/client_key pair must dial with the client pair.
func TestLoadClientTLSPrefersClientCert(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with split client cert failed: %v", err)
}
}
// Without client_cert, the component keeps presenting its serving cert.
func TestLoadClientTLSFallsBackToServingCert(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
dualCert, dualKey := ca.issue(t, dir, "dual", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", dualCert)
v.Set("grpc.master.key", dualKey)
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with dual-EKU cert failed: %v", err)
}
}
// A serverAuth-only cert presented as the client identity fails the peer's
// clientAuth EKU verification — the failure mode client_cert exists to fix.
func TestLoadClientTLSServerOnlyEkuRejected(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err == nil {
t.Fatal("expected handshake failure when presenting a serverAuth-only cert as client identity")
}
}
func TestLoadServerTLSUnconfiguredIsPlaintext(t *testing.T) {
v := util.NewViperProxy(viper.New())
creds, _, err := loadServerTLS(v, "grpc.master")
if err != nil || creds != nil {
t.Fatalf("expected no TLS and no error, got creds=%v err=%v", creds, err)
}
}
func TestLoadServerTLSPartialCertKeyFails(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
certFile, _ := ca.issue(t, dir, "server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", certFile)
if _, _, err := loadServerTLS(v, "grpc.master"); err == nil {
t.Fatal("expected error when only cert is set")
}
}
func TestLoadServerTLSBadVersionNameFails(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
certFile, keyFile := ca.issue(t, dir, "server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", certFile)
v.Set("grpc.master.key", keyFile)
v.Set("tls.min_version", "bogus")
if _, _, err := loadServerTLS(v, "grpc.master"); err == nil {
t.Fatal("expected error for unparseable tls.min_version")
}
}
func TestLoadClientTLSUnconfiguredIsPlaintext(t *testing.T) {
v := util.NewViperProxy(viper.New())
if _, err := loadClientTLS(v, "grpc.client", "grpc.client"); err != nil {
t.Fatalf("expected insecure dial option, got err=%v", err)
}
}
// A dedicated client pair missing one half is a misconfiguration: the dialer
// must fail rather than silently dial with the fallback serving identity.
func TestLoadClientTLSPartialClientPairFails(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
certFile, keyFile := ca.issue(t, dir, "dual", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
clientCert, _ := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.client.cert", certFile)
v.Set("grpc.client.key", keyFile)
v.Set("grpc.client.client_cert", clientCert)
if _, err := loadClientTLS(v, "grpc.client", "grpc.client"); err == nil {
t.Fatal("expected error for a partial client_cert/client_key pair with a valid fallback")
}
}
func TestLoadClientTLSMissingCaFails(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
certFile, keyFile := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.client.cert", certFile)
v.Set("grpc.client.key", keyFile)
if _, err := loadClientTLS(v, "grpc.client", "grpc.client"); err == nil {
t.Fatal("expected error when cert and key are set without grpc.ca")
}
}
func TestLoadClientTLSBadCaPathFails(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
certFile, keyFile := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", filepath.Join(dir, "missing-ca.pem"))
v.Set("grpc.client.cert", certFile)
v.Set("grpc.client.key", keyFile)
if _, err := loadClientTLS(v, "grpc.client", "grpc.client"); err == nil {
t.Fatal("expected error for unreadable grpc.ca")
}
}
func TestLoadClientTLSAllowsListedServerName(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "seaweed-filer", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.master.allowed_server_commonNames", "seaweed-filer,seaweed-volume")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with listed server name failed: %v", err)
}
}
// Any CA-signed cert must not be able to stand in for another component:
// a filer's identity dialing as "the master" has to fail the allowlist.
func TestLoadClientTLSRejectsUnlistedServerName(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "seaweed-filer", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.master.allowed_server_commonNames", "seaweed-master")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err == nil {
t.Fatal("expected handshake failure for a server identity outside the allowlist")
}
}
func TestLoadClientTLSMatchesServerURISAN(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issueWithSANs(t, dir, "server",
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, nil, []string{"spiffe://cluster.local/ns/seaweedfs/master"})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.master.allowed_server_commonNames", "spiffe://cluster.local/ns/seaweedfs/master")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with URI SAN allowlist failed: %v", err)
}
}
func TestLoadClientTLSMatchesServerWildcardDomain(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issueWithSANs(t, dir, "server",
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, []string{"master.example.com"}, nil)
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.allowed_server_wildcard_domain", ".example.com")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with wildcard domain failed: %v", err)
}
}
func TestMatchesWildcardDomain(t *testing.T) {
cases := []struct {
name string
domain string
want bool
}{
{"node.prod.example.com", ".example.com", true},
{"node.example.com.", ".example.com", true},
{"node.example.com.", ".example.com.", true},
{"node.example.com..", ".example.com", false},
{"node..example.com.", ".example.com", false},
{"example.com", ".example.com", false},
{".example.com", ".example.com", false},
{"node..example.com", ".example.com", false},
{"node-.example.com", ".example.com", false},
{"-node.example.com", ".example.com", false},
{"node.example.com", "", false},
{"node.example.com", ".other.net", false},
{"badexample.com", "example.com", false},
{"good.example.com", "example.com", true},
{"*.example.com", ".example.com", true},
{"1.2.3.4", ".3.4", false},
}
for _, c := range cases {
if got := matchesWildcardDomain(c.name, c.domain); got != c.want {
t.Errorf("matchesWildcardDomain(%q, %q) = %v, want %v", c.name, c.domain, got, c.want)
}
}
}
// A CA-signed name that is not a valid hostname must not satisfy the wildcard
// suffix check: ".example.com" equals the wildcard and has no hostname label.
func TestLoadClientTLSRejectsInvalidWildcardName(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issueWithSANs(t, dir, ".example.com",
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, []string{".example.com"}, nil)
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.allowed_server_wildcard_domain", ".example.com")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err == nil {
t.Fatal("expected handshake failure: a non-hostname identity must not match the wildcard domain")
}
}
// A URI SAN whose path happens to end in the wildcard domain must not pass:
// URI identities are exact-match only.
func TestLoadClientTLSURIPathDoesNotMatchWildcardDomain(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issueWithSANs(t, dir, "server",
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, nil, []string{"spiffe://other.invalid/workload.example.com"})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.allowed_server_wildcard_domain", ".example.com")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err == nil {
t.Fatal("expected handshake failure: URI SAN path must not match the wildcard domain")
}
}
func TestLoadClientTLSGlobalServerNamesFallback(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "seaweed-filer", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
v.Set("grpc.allowed_server_commonNames", "seaweed-filer")
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with global allowlist failed: %v", err)
}
}
// A per-target allowlist lives under the dialing component: the filer reads
// grpc.filer.volume_allowed_server_commonNames for volume calls, never
// grpc.volume.allowed_server_commonNames, which is the volume's own outbound
// allowlist (e.g. naming only the master).
func TestLoadClientTLSForTargetUsesClientScopedAllowlist(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "seaweed-volume", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.volume.cert", serverCert)
v.Set("grpc.volume.key", serverKey)
v.Set("grpc.volume.allowed_server_commonNames", "seaweed-master")
v.Set("grpc.filer.client_cert", clientCert)
v.Set("grpc.filer.client_key", clientKey)
v.Set("grpc.filer.volume_allowed_server_commonNames", "seaweed-volume")
addr := startTestGrpcServer(t, v, "grpc.volume")
if err := healthCheck(t, addr, LoadClientTLSForTarget(v, "grpc.filer", "grpc.volume")); err != nil {
t.Fatalf("health check with client-scoped target allowlist failed: %v", err)
}
}
func TestLoadClientTLSForTargetIgnoresTargetOutboundAllowlist(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "seaweed-volume", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.volume.cert", serverCert)
v.Set("grpc.volume.key", serverKey)
v.Set("grpc.volume.allowed_server_commonNames", "seaweed-master")
v.Set("grpc.filer.client_cert", clientCert)
v.Set("grpc.filer.client_key", clientKey)
addr := startTestGrpcServer(t, v, "grpc.volume")
if err := healthCheck(t, addr, LoadClientTLSForTarget(v, "grpc.filer", "grpc.volume")); err != nil {
t.Fatalf("health check must not apply the target's outbound allowlist: %v", err)
}
}
func TestLoadClientTLSForTargetFallsBackToClientAllowlist(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "seaweed-volume", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.volume.cert", serverCert)
v.Set("grpc.volume.key", serverKey)
v.Set("grpc.filer.client_cert", clientCert)
v.Set("grpc.filer.client_key", clientKey)
v.Set("grpc.filer.allowed_server_commonNames", "seaweed-volume")
addr := startTestGrpcServer(t, v, "grpc.volume")
if err := healthCheck(t, addr, LoadClientTLSForTarget(v, "grpc.filer", "grpc.volume")); err != nil {
t.Fatalf("health check with client component allowlist failed: %v", err)
}
}