From adfcb7d2ffb666b57688949921d17a84f8d5bf14 Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Sat, 14 Mar 2026 20:09:19 -0400 Subject: [PATCH] [mint] support Deny directives in ACLs --- mint/servicer/BUILD.bazel | 1 + mint/servicer/acl.go | 54 +++++++++++++++++++++++++++++---------- mint/servicer/acl_test.go | 4 +-- mint/servicer/servicer.go | 3 ++- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/mint/servicer/BUILD.bazel b/mint/servicer/BUILD.bazel index 416ed0f..9090ca0 100644 --- a/mint/servicer/BUILD.bazel +++ b/mint/servicer/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//utils/hostname", "//utils/log", "//utils/option", + "//utils/stringmatch", "@com_github_smallstep_certificates//api", "@com_github_smallstep_certificates//ca", "@com_github_smallstep_cli//token", diff --git a/mint/servicer/acl.go b/mint/servicer/acl.go index f0cce04..3958695 100644 --- a/mint/servicer/acl.go +++ b/mint/servicer/acl.go @@ -8,6 +8,7 @@ import ( "go.fuhry.dev/runtime/mtls" "go.fuhry.dev/runtime/utils/log" + "go.fuhry.dev/runtime/utils/stringmatch" "gopkg.in/yaml.v3" ) @@ -16,6 +17,7 @@ var defaultAclPath = "/ephs/local/services/mint/rules.yaml" type AclRule struct { RemoteId mtls.RemoteIdentity Allow string + Deny string } type TemplateRule struct { @@ -30,12 +32,21 @@ type AclRulesFile struct { var _ yaml.Unmarshaler = &AclRule{} +type decision uint + +const ( + noDecision decision = iota + decisionAllow + decisionDeny +) + func (r *AclRule) UnmarshalYAML(node *yaml.Node) error { type unpacked struct { User string `yaml:"user"` Service string `yaml:"service"` Domain string `yaml:"domain"` Allow string `yaml:"allow"` + Deny string `yaml:"deny"` } u := new(unpacked) @@ -48,8 +59,12 @@ func (r *AclRule) UnmarshalYAML(node *yaml.Node) error { return errors.New("user and service are mutually exclusive") } - if u.Allow == "" { - return errors.New("property \"allow\" is required") + if u.Allow == "" && u.Deny == "" { + return errors.New("either \"allow\" or \"deny\" is required") + } + + if u.Allow != "" && u.Deny != "" { + return errors.New("\"allow\" and \"deny\" are mutually exclusive") } r.RemoteId = mtls.RemoteIdentity{ @@ -68,15 +83,21 @@ func (r *AclRule) UnmarshalYAML(node *yaml.Node) error { } r.Allow = u.Allow + r.Deny = u.Deny return nil } -func (r *AclRule) Check(id *mtls.RemoteIdentity, requestedPrincipal string) bool { +func (r *AclRule) Check(id *mtls.RemoteIdentity, requestedPrincipal string) decision { logger := log.Default().WithPrefix(fmt.Sprintf("mint.servicer.Acl[%s, %s]", r.RemoteId.ToSpiffe().String(), r.Allow)) - if r.Allow != "*" && r.Allow != requestedPrincipal { + onMatch := noDecision + if r.Allow == "*" || r.Allow == requestedPrincipal { + onMatch = decisionAllow + } else if r.Deny == "*" || r.Deny == requestedPrincipal { + onMatch = decisionDeny + } else { logger.Infof("Check(%q, %q) -> false (princ mismatch)", id.ToSpiffe().String(), requestedPrincipal) - return false + return noDecision } switch r.RemoteId.Class { @@ -84,39 +105,46 @@ func (r *AclRule) Check(id *mtls.RemoteIdentity, requestedPrincipal string) bool if r.RemoteId.Domain != "" { if id.Domain != r.RemoteId.Domain && !strings.HasSuffix(id.Domain, "."+r.RemoteId.Domain) { logger.Infof("Check(%q, %q) -> false (domain mismatch)", id.ToSpiffe().String(), requestedPrincipal) - return false + return noDecision } } if id.Class != r.RemoteId.Class { logger.Infof("Check(%q, %q) -> false (class mismatch)", id.ToSpiffe().String(), requestedPrincipal) - return false + return noDecision } if r.RemoteId.Principal != "*" && id.Principal != r.RemoteId.Principal { logger.Infof("Check(%q, %q) -> false (requester princ mismatch)", id.ToSpiffe().String(), requestedPrincipal) - return false + return noDecision } logger.Infof("Check(%q, %q) -> true", id.ToSpiffe().String(), requestedPrincipal) - return true + return onMatch case mtls.Domain: if r.RemoteId.Domain == "" { logger.Infof("Check(%q, %q) -> false (domain mismatch)", id.ToSpiffe().String(), requestedPrincipal) - return false + return noDecision } match := id.Domain == r.RemoteId.Domain || strings.HasSuffix(id.Domain, "."+r.RemoteId.Domain) logger.Infof("Check(%q, %q) -> %t (domain check)", id.ToSpiffe().String(), requestedPrincipal, match) - return match + if match { + return onMatch + } + return noDecision } logger.Infof("Check(%q, %q) -> false (fallback case)", id.ToSpiffe().String(), requestedPrincipal) - return false + return noDecision } func (f *AclRulesFile) Check(id *mtls.RemoteIdentity, requestedPrincipal string) bool { for _, r := range f.Rules { - if r.Check(id, requestedPrincipal) { + d := r.Check(id, requestedPrincipal) + if d == decisionAllow { return true + } else if d == decisionDeny { + return false } + // else, continue processing } return false diff --git a/mint/servicer/acl_test.go b/mint/servicer/acl_test.go index 0c04e87..b7cf777 100644 --- a/mint/servicer/acl_test.go +++ b/mint/servicer/acl_test.go @@ -36,7 +36,7 @@ func TestAcl(t *testing.T) { desc, user, service, domain, allow string id mtls.RemoteIdentity princ string - expect bool + expect decision } var testCases = []testCase{ @@ -47,7 +47,7 @@ func TestAcl(t *testing.T) { allow: "*", id: mtls.RemoteIdentity{mtls.User, "example.com", "bob"}, princ: "foo-service", - expect: true, + expect: decisionAllow, }, } diff --git a/mint/servicer/servicer.go b/mint/servicer/servicer.go index aab44f7..660c719 100644 --- a/mint/servicer/servicer.go +++ b/mint/servicer/servicer.go @@ -145,7 +145,8 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C return nil, status.Errorf(codes.PermissionDenied, "no service is allowed to assume its own identity") } - if !s.acl.Check(remoteId, req.RequestedIdentity) { + ok := s.acl.Check(remoteId, req.RequestedIdentity) + if !ok { return nil, status.Errorf(codes.PermissionDenied, "%s is not allowed to assume identity %q", remoteId.ToSpiffe().String(), -- 2.52.0