]> go.fuhry.dev Git - runtime.git/commitdiff
[mint] support Deny directives in ACLs
authorDan Fuhry <dan@fuhry.com>
Sun, 15 Mar 2026 00:09:19 +0000 (20:09 -0400)
committerDan Fuhry <dan@fuhry.com>
Sun, 15 Mar 2026 01:17:55 +0000 (21:17 -0400)
mint/servicer/BUILD.bazel
mint/servicer/acl.go
mint/servicer/acl_test.go
mint/servicer/servicer.go

index 416ed0ff9a576e450e70e334e02772694b5c88e5..9090ca00bde329f21c53251bb63db61d13c18735 100644 (file)
@@ -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",
index f0cce040463ee83940f054affe5b6330e6a2365c..3958695989e2820d61b45f19781cb556148debb3 100644 (file)
@@ -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
index 0c04e87d5b0f91c588a0c0c0ab9a7a529ccc2dca..b7cf777f375f0d8ddfccceab90d4361b0e463441 100644 (file)
@@ -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,
                },
        }
 
index aab44f70f53ec1cbce7174492ca6416531347c53..660c719b040b7fe7a8dadcfc7b8bba76090db08b 100644 (file)
@@ -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(),