v0.17.0 mtls added to network chain, sinks, and sources
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"logwisp/internal/config"
|
||||
"logwisp/internal/tlsx"
|
||||
)
|
||||
|
||||
// peerState fakes a completed handshake. Only the leaf's identity fields are
|
||||
// read: the chain is verified by crypto/tls before a policy ever sees it.
|
||||
func peerState(leaf *x509.Certificate) *tls.ConnectionState {
|
||||
return &tls.ConnectionState{PeerCertificates: []*x509.Certificate{leaf}}
|
||||
}
|
||||
|
||||
func leafCN(cn string) *x509.Certificate {
|
||||
return &x509.Certificate{Subject: pkix.Name{CommonName: cn}}
|
||||
}
|
||||
|
||||
func mtlsListenerTLS() *config.TLSOptions {
|
||||
return &config.TLSOptions{Enabled: true, ClientAuth: true}
|
||||
}
|
||||
|
||||
func TestPeerIdentityModes(t *testing.T) {
|
||||
uri, err := url.Parse("spiffe://example.org/edge-01")
|
||||
if err != nil {
|
||||
t.Fatalf("parse uri: %v", err)
|
||||
}
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "edge-01"},
|
||||
DNSNames: []string{"edge-01.internal", "alt.internal"},
|
||||
URIs: []*url.URL{uri},
|
||||
EmailAddresses: []string{"ops@example.org"},
|
||||
}
|
||||
cs := peerState(leaf)
|
||||
|
||||
cases := map[string]string{
|
||||
tlsx.IdentityCN: "edge-01",
|
||||
tlsx.IdentitySANDNS: "edge-01.internal",
|
||||
tlsx.IdentitySANURI: "spiffe://example.org/edge-01",
|
||||
tlsx.IdentitySANEmail: "ops@example.org",
|
||||
"": "edge-01", // empty mode defaults to CN
|
||||
"nonsense": "",
|
||||
}
|
||||
for mode, want := range cases {
|
||||
if got := tlsx.PeerIdentity(*cs, mode); got != want {
|
||||
t.Errorf("PeerIdentity(%q) = %q, want %q", mode, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A mode the certificate does not carry yields no identity
|
||||
bare := peerState(leafCN("edge-01"))
|
||||
if got := tlsx.PeerIdentity(*bare, tlsx.IdentitySANDNS); got != "" {
|
||||
t.Errorf("PeerIdentity(san_dns) on bare cert = %q, want empty", got)
|
||||
}
|
||||
// No peer certificate at all
|
||||
if got := tlsx.PeerIdentity(tls.ConnectionState{}, tlsx.IdentityCN); got != "" {
|
||||
t.Errorf("PeerIdentity with no peer certs = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDisabled(t *testing.T) {
|
||||
for _, o := range []*config.AuthOptions{nil, {}, {Type: MethodNone}} {
|
||||
p, err := New(o, nil, RoleListener)
|
||||
if err != nil {
|
||||
t.Fatalf("New(%+v) error: %v", o, err)
|
||||
}
|
||||
if p != nil {
|
||||
t.Fatalf("New(%+v) = %v, want nil policy", o, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A nil policy must behave as if auth were never configured
|
||||
func TestNilPolicyIsTransparent(t *testing.T) {
|
||||
var p *Policy
|
||||
id, err := p.Authorize(nil)
|
||||
if err != nil || id.Name != "" {
|
||||
t.Fatalf("nil Authorize = (%+v, %v), want (zero, nil)", id, err)
|
||||
}
|
||||
if p.Enabled() || p.BindsNode() || p.Unrestricted() {
|
||||
t.Fatal("nil policy reports itself active")
|
||||
}
|
||||
if p.NodeBinding() != BindingNone {
|
||||
t.Fatalf("nil NodeBinding = %q", p.NodeBinding())
|
||||
}
|
||||
if !p.TrustsEntryNode(true) || p.TrustsEntryNode(false) {
|
||||
t.Fatal("nil policy must defer to trust_node")
|
||||
}
|
||||
// trust_node semantics are unchanged without a policy
|
||||
node, err := p.ResolveNode("edge-01", "10.0.0.5", true, Identity{})
|
||||
if err != nil || node != "edge-01" {
|
||||
t.Fatalf("nil ResolveNode(trust) = (%q, %v), want edge-01", node, err)
|
||||
}
|
||||
node, err = p.ResolveNode("edge-01", "10.0.0.5", false, Identity{})
|
||||
if err != nil || node != "10.0.0.5" {
|
||||
t.Fatalf("nil ResolveNode(no trust) = (%q, %v), want 10.0.0.5", node, err)
|
||||
}
|
||||
node, err = p.ResolveNode("", "10.0.0.5", true, Identity{})
|
||||
if err != nil || node != "10.0.0.5" {
|
||||
t.Fatalf("nil ResolveNode(no label) = (%q, %v), want 10.0.0.5", node, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
auth *config.AuthOptions
|
||||
tls *config.TLSOptions
|
||||
role Role
|
||||
}{
|
||||
{"unknown type", &config.AuthOptions{Type: "kerberos"}, mtlsListenerTLS(), RoleListener},
|
||||
{"no tls", &config.AuthOptions{Type: MethodMTLS}, nil, RoleListener},
|
||||
{"tls disabled", &config.AuthOptions{Type: MethodMTLS}, &config.TLSOptions{}, RoleListener},
|
||||
{"no client_auth", &config.AuthOptions{Type: MethodMTLS}, &config.TLSOptions{Enabled: true}, RoleListener},
|
||||
{"unknown identity", &config.AuthOptions{Type: MethodMTLS, Identity: "serial"}, mtlsListenerTLS(), RoleListener},
|
||||
{"bad pattern", &config.AuthOptions{Type: MethodMTLS, AllowPatterns: []string{"^edge-("}}, mtlsListenerTLS(), RoleListener},
|
||||
{"unknown binding", &config.AuthOptions{Type: MethodMTLS, NodeBinding: "maybe"}, mtlsListenerTLS(), RoleChainListener},
|
||||
{"binding on plain listener", &config.AuthOptions{Type: MethodMTLS, NodeBinding: BindingForce}, mtlsListenerTLS(), RoleListener},
|
||||
{"binding on dialer", &config.AuthOptions{Type: MethodMTLS, NodeBinding: BindingForce}, &config.TLSOptions{Enabled: true}, RoleDialer},
|
||||
{"dialer skips verify", &config.AuthOptions{Type: MethodMTLS}, &config.TLSOptions{Enabled: true, InsecureSkipVerify: true}, RoleDialer},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := New(tc.auth, tc.tls, tc.role); err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A dialer needs TLS but not client_auth: it pins the server's identity
|
||||
if _, err := New(&config.AuthOptions{Type: MethodMTLS}, &config.TLSOptions{Enabled: true}, RoleDialer); err != nil {
|
||||
t.Fatalf("dialer policy rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeMatching(t *testing.T) {
|
||||
p, err := New(&config.AuthOptions{
|
||||
Type: MethodMTLS,
|
||||
Allow: []string{"edge-01", " edge-02 "},
|
||||
AllowPatterns: []string{`^relay-\d{2}$`},
|
||||
}, mtlsListenerTLS(), RoleListener)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if p.Unrestricted() {
|
||||
t.Fatal("policy with an allow list reports unrestricted")
|
||||
}
|
||||
|
||||
allowed := []string{"edge-01", "edge-02", "relay-07"}
|
||||
for _, cn := range allowed {
|
||||
id, err := p.Authorize(peerState(leafCN(cn)))
|
||||
if err != nil {
|
||||
t.Errorf("Authorize(%q): %v", cn, err)
|
||||
continue
|
||||
}
|
||||
if id.Name != cn || id.Method != MethodMTLS {
|
||||
t.Errorf("Authorize(%q) = %+v", cn, id)
|
||||
}
|
||||
}
|
||||
|
||||
denied := []string{"edge-99", "relay-007", "prefix-relay-07", "", "EDGE-01"}
|
||||
for _, cn := range denied {
|
||||
if _, err := p.Authorize(peerState(leafCN(cn))); err == nil {
|
||||
t.Errorf("Authorize(%q) allowed, want rejection", cn)
|
||||
}
|
||||
}
|
||||
|
||||
if got, want := p.Rejected(), uint64(len(denied)); got != want {
|
||||
t.Errorf("Rejected = %d, want %d", got, want)
|
||||
}
|
||||
stats := p.Stats()
|
||||
if stats["auth_allowed"].(uint64) != uint64(len(allowed)) {
|
||||
t.Errorf("auth_allowed = %v, want %d", stats["auth_allowed"], len(allowed))
|
||||
}
|
||||
if _, ok := stats["node_binding"]; ok {
|
||||
t.Error("plain listener stats report node_binding")
|
||||
}
|
||||
}
|
||||
|
||||
// Empty allow and allow_patterns admits any CA-vouched identity, but still
|
||||
// records it and still refuses a certificate with no usable identity field
|
||||
func TestAuthorizeUnrestricted(t *testing.T) {
|
||||
p, err := New(&config.AuthOptions{Type: MethodMTLS}, mtlsListenerTLS(), RoleListener)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if !p.Unrestricted() {
|
||||
t.Fatal("empty allow list should be unrestricted")
|
||||
}
|
||||
id, err := p.Authorize(peerState(leafCN("anyone")))
|
||||
if err != nil || id.Name != "anyone" {
|
||||
t.Fatalf("Authorize = (%+v, %v)", id, err)
|
||||
}
|
||||
if _, err := p.Authorize(peerState(leafCN(""))); err == nil {
|
||||
t.Error("certificate with no CN was authorized")
|
||||
}
|
||||
if _, err := p.Authorize(&tls.ConnectionState{}); err == nil {
|
||||
t.Error("connection with no peer certificate was authorized")
|
||||
}
|
||||
if _, err := p.Authorize(nil); err == nil {
|
||||
t.Error("non-TLS connection was authorized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNodeBindings(t *testing.T) {
|
||||
newChain := func(binding string) *Policy {
|
||||
p, err := New(&config.AuthOptions{Type: MethodMTLS, NodeBinding: binding}, mtlsListenerTLS(), RoleChainListener)
|
||||
if err != nil {
|
||||
t.Fatalf("New(%q): %v", binding, err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
id := Identity{Name: "edge-01", Method: MethodMTLS}
|
||||
|
||||
// Default under mtls is force
|
||||
if got := newChain("").NodeBinding(); got != BindingForce {
|
||||
t.Errorf("default node_binding = %q, want %q", got, BindingForce)
|
||||
}
|
||||
|
||||
// force ignores the declared label, however it was spoofed
|
||||
force := newChain(BindingForce)
|
||||
for _, declared := range []string{"edge-99", "", "edge-01"} {
|
||||
node, err := force.ResolveNode(declared, "10.0.0.5", true, id)
|
||||
if err != nil || node != "edge-01" {
|
||||
t.Errorf("force ResolveNode(%q) = (%q, %v), want edge-01", declared, node, err)
|
||||
}
|
||||
}
|
||||
if force.TrustsEntryNode(true) {
|
||||
t.Error("force must not trust per-entry node labels")
|
||||
}
|
||||
|
||||
// assert rejects a mismatch and an omission, and leaves per-entry labels
|
||||
// alone so a relay can forward other nodes' entries
|
||||
assert := newChain(BindingAssert)
|
||||
node, err := assert.ResolveNode("edge-01", "10.0.0.5", true, id)
|
||||
if err != nil || node != "edge-01" {
|
||||
t.Errorf("assert ResolveNode(match) = (%q, %v)", node, err)
|
||||
}
|
||||
if _, err := assert.ResolveNode("edge-99", "10.0.0.5", true, id); err == nil {
|
||||
t.Error("assert accepted a mismatched node label")
|
||||
}
|
||||
if _, err := assert.ResolveNode("", "10.0.0.5", true, id); err == nil {
|
||||
t.Error("assert accepted a missing node label")
|
||||
}
|
||||
if !assert.TrustsEntryNode(true) || assert.TrustsEntryNode(false) {
|
||||
t.Error("assert must leave per-entry node labels to trust_node")
|
||||
}
|
||||
|
||||
// none leaves trust_node governing entirely
|
||||
none := newChain(BindingNone)
|
||||
if none.BindsNode() {
|
||||
t.Error("node_binding none should not bind")
|
||||
}
|
||||
node, err = none.ResolveNode("edge-99", "10.0.0.5", true, id)
|
||||
if err != nil || node != "edge-99" {
|
||||
t.Errorf("none ResolveNode = (%q, %v), want edge-99", node, err)
|
||||
}
|
||||
node, err = none.ResolveNode("edge-99", "10.0.0.5", false, id)
|
||||
if err != nil || node != "10.0.0.5" {
|
||||
t.Errorf("none ResolveNode(no trust) = (%q, %v), want 10.0.0.5", node, err)
|
||||
}
|
||||
|
||||
// Binding without an authenticated identity is a refusal, not a fallback
|
||||
if _, err := force.ResolveNode("edge-01", "10.0.0.5", true, Identity{}); err == nil {
|
||||
t.Error("force resolved a node without an identity")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityApply(t *testing.T) {
|
||||
meta := map[string]any{"type": "tcp_chain"}
|
||||
Identity{}.Apply(meta)
|
||||
if len(meta) != 1 {
|
||||
t.Fatalf("zero identity stamped metadata: %v", meta)
|
||||
}
|
||||
Identity{Name: "edge-01", Method: MethodMTLS}.Apply(meta)
|
||||
if meta["auth_identity"] != "edge-01" || meta["auth_method"] != MethodMTLS {
|
||||
t.Fatalf("metadata = %v", meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyConnectionPinsServer(t *testing.T) {
|
||||
p, err := New(&config.AuthOptions{Type: MethodMTLS, Allow: []string{"relay.internal"}},
|
||||
&config.TLSOptions{Enabled: true}, RoleDialer)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if err := p.VerifyConnection(*peerState(leafCN("relay.internal"))); err != nil {
|
||||
t.Errorf("pinned server rejected: %v", err)
|
||||
}
|
||||
if err := p.VerifyConnection(*peerState(leafCN("impostor.internal"))); err == nil {
|
||||
t.Error("unpinned server accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user