]> go.fuhry.dev Git - runtime.git/commitdiff
[mint] Send hostname and domain name, add templates
authorDan Fuhry <dan@fuhry.com>
Sat, 14 Mar 2026 23:50:51 +0000 (19:50 -0400)
committerDan Fuhry <dan@fuhry.com>
Sun, 15 Mar 2026 01:17:54 +0000 (21:17 -0400)
- Add templates for custom SANs derived from host info sent in gRPC req
- Supporting changes to proto, client, etc.

mint/BUILD.bazel
mint/client.go
mint/servicer/BUILD.bazel
mint/servicer/acl.go
mint/servicer/servicer.go
mint/servicer/signer.go
proto/service/mint/mint_types.pb.go
proto/service/mint/mint_types.proto

index 22822d38426b1a696aa83a034a15e7d5402133c6..d5a9662932fe32563fb88374df946e4f8fc81cec 100644 (file)
@@ -14,6 +14,7 @@ go_library(
         "//mtls/certutil",
         "//proto/service/mint",
         "//utils/context",
+        "//utils/hostname",
         "//utils/log",
         "//utils/option",
         "@com_github_quic_go_quic_go//:quic-go",
index 6c1f116e9fa67652a578686fff4a4fb34aa77fbd..932e544671ddca698e600f1ff43066a9e675c574 100644 (file)
@@ -19,6 +19,7 @@ import (
        "go.fuhry.dev/runtime/mtls/certutil"
        mintpb "go.fuhry.dev/runtime/proto/service/mint"
        "go.fuhry.dev/runtime/utils/context"
+       "go.fuhry.dev/runtime/utils/hostname"
        "go.fuhry.dev/runtime/utils/log"
        "go.fuhry.dev/runtime/utils/option"
 )
@@ -155,6 +156,10 @@ func (c *clientImpl) GetCertificate(subject string) (*MintCertificate, error) {
        cr := &mintpb.CertificateRequest{
                RequestedIdentity: subject,
                PublicKey:         publicKey,
+               HostInfo: &mintpb.CertificateRequest_HostInfo{
+                       Hostname: hostname.Hostname(),
+                       Domain:   hostname.DomainName(),
+               },
        }
 
        resp, err := rpc.RequestCertificate(c.defaultCtx, cr)
index 44c0086297254e76f333ff7fd030b9959eb48ee7..874482c9577b86d37ae1a5473e43787487049699 100644 (file)
@@ -18,6 +18,7 @@ go_library(
         "//mtls/certutil",
         "//proto/service/mint",
         "//utils/context",
+        "//utils/hostname",
         "//utils/log",
         "//utils/option",
         "@com_github_smallstep_certificates//api",
index 04d9224d05b354c277bb38e638d6f561e6aefbe5..f0cce040463ee83940f054affe5b6330e6a2365c 100644 (file)
@@ -18,8 +18,14 @@ type AclRule struct {
        Allow    string
 }
 
+type TemplateRule struct {
+       Principal stringmatch.NewSyntaxMatchRule `yaml:"principal"`
+       Names     []string                       `yaml:"names"`
+}
+
 type AclRulesFile struct {
-       Rules []*AclRule `yaml:"rules"`
+       Rules     []*AclRule      `yaml:"rules"`
+       Templates []*TemplateRule `yaml:"templates"`
 }
 
 var _ yaml.Unmarshaler = &AclRule{}
@@ -116,6 +122,21 @@ func (f *AclRulesFile) Check(id *mtls.RemoteIdentity, requestedPrincipal string)
        return false
 }
 
+func (f *AclRulesFile) Names(requestedPrincipal string) (out []string) {
+       out = []string{
+               "spiffe://{{spiffe.domain}}/service/{{principal}}",
+       }
+
+       for _, rule := range f.Templates {
+               if rule.Principal.Match(requestedPrincipal) {
+                       out = rule.Names
+                       break
+               }
+       }
+
+       return
+}
+
 func init() {
        flag.StringVar(&defaultAclPath, "mint.acl-path", defaultAclPath, "local or ephs path to rules file that dictates who can request what certificates")
 }
index dfbc4fb1ebe294c7f179a00276645a3278bfaa01..aab44f70f53ec1cbce7174492ca6416531347c53 100644 (file)
@@ -5,6 +5,7 @@ import (
        "crypto/x509"
        "io"
        "os"
+       "strings"
 
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/status"
@@ -18,6 +19,7 @@ import (
        "go.fuhry.dev/runtime/mtls/certutil"
        mint_proto "go.fuhry.dev/runtime/proto/service/mint"
        "go.fuhry.dev/runtime/utils/context"
+       "go.fuhry.dev/runtime/utils/hostname"
        "go.fuhry.dev/runtime/utils/log"
        "go.fuhry.dev/runtime/utils/option"
 )
@@ -121,6 +123,14 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C
                return nil, status.Errorf(codes.Unauthenticated, "cannot determine peer certificate")
        }
 
+       if !hostname.ValidHostname.MatchString(req.HostInfo.Hostname) {
+               return nil, status.Errorf(codes.InvalidArgument, "provided hostname %q is invalid", req.HostInfo.Hostname)
+       }
+
+       if !hostname.ValidDomainName.MatchString(req.HostInfo.Domain) {
+               return nil, status.Errorf(codes.InvalidArgument, "provided domain name %q is invalid", req.HostInfo.Domain)
+       }
+
        spiffeId := certutil.SpiffeUrlFromCertificate(peerCert)
        if spiffeId == nil {
                return nil, status.Errorf(codes.Unauthenticated, "cannot find SPIFFE ID in peer certificate")
@@ -142,6 +152,21 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C
                        req.RequestedIdentity)
        }
 
+       template := s.acl.Names(req.RequestedIdentity)
+       kv := map[string]string{
+               "spiffe.domain":    remoteId.Domain,
+               "spiffe.class":     remoteId.Class.String(),
+               "spiffe.principal": remoteId.Principal,
+               "principal":        req.RequestedIdentity,
+               "hostname":         req.HostInfo.Hostname,
+               "domain":           req.HostInfo.Domain,
+       }
+       for i := range template {
+               for k, v := range kv {
+                       template[i] = strings.ReplaceAll(template[i], "{{"+k+"}}", v)
+               }
+       }
+
        pub, err := x509.ParsePKIXPublicKey(req.PublicKey)
        if err != nil {
                return nil, status.Errorf(codes.InvalidArgument, "failed to parse public key: %v", err)
@@ -153,7 +178,7 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C
                Principal: req.RequestedIdentity,
        }
 
-       csr, sr, err := CreateSignRequest(requestedId, pub)
+       csr, sr, err := CreateSignRequest(requestedId, template, pub)
        if err != nil {
                return nil, status.Errorf(codes.Internal, "failed to create sign request: %v", err)
        }
index b0b07bf480908bcdedd8fd1598e0a510570cba8b..30dc1c7435cfea86d3175c2e6dc5256ab2cb5181 100644 (file)
@@ -30,6 +30,7 @@ import (
        "go.fuhry.dev/runtime/mint/remote_signer"
        "go.fuhry.dev/runtime/mtls"
        "go.fuhry.dev/runtime/utils/context"
+       "go.fuhry.dev/runtime/utils/hostname"
        "go.fuhry.dev/runtime/utils/log"
 )
 
@@ -125,7 +126,7 @@ func withRootSHA(hash string) token.Options {
        }
 }
 
-func CreateSignRequest(subject *mtls.RemoteIdentity, pub crypto.PublicKey) (*x509.CertificateRequest, *remote_signer.SignRequest, error) {
+func CreateSignRequest(subject *mtls.RemoteIdentity, extraNames []string, pub crypto.PublicKey) (*x509.CertificateRequest, *remote_signer.SignRequest, error) {
        var alg x509.PublicKeyAlgorithm
        switch pub.(type) {
        case *ecdsa.PublicKey:
@@ -152,6 +153,14 @@ func CreateSignRequest(subject *mtls.RemoteIdentity, pub crypto.PublicKey) (*x50
                },
        }
 
+       for _, n := range extraNames {
+               if nUrl, err := url.Parse(n); err == nil && nUrl.Scheme != "" {
+                       csrTemplate.URIs = append(csrTemplate.URIs, nUrl)
+               } else if hostname.ValidDomainName.MatchString(n) {
+                       csrTemplate.DNSNames = append(csrTemplate.DNSNames, n)
+               }
+       }
+
        rs := remote_signer.NewSigner(pub)
        _, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, rs)
        if !errors.Is(err, remote_signer.ErrSignerNonlocal) {
index 48731be0953ec588eb3ca5c47d4c1348a72fccf2..8bf832870eed5eb9b97e1f18c4e59d3f212c2cfc 100644 (file)
@@ -22,9 +22,10 @@ const (
 )
 
 type CertificateRequest struct {
-       state             protoimpl.MessageState `protogen:"open.v1"`
-       RequestedIdentity string                 `protobuf:"bytes,1,opt,name=requested_identity,json=requestedIdentity,proto3" json:"requested_identity,omitempty"`
-       PublicKey         []byte                 `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+       state             protoimpl.MessageState       `protogen:"open.v1"`
+       RequestedIdentity string                       `protobuf:"bytes,1,opt,name=requested_identity,json=requestedIdentity,proto3" json:"requested_identity,omitempty"`
+       PublicKey         []byte                       `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+       HostInfo          *CertificateRequest_HostInfo `protobuf:"bytes,3,opt,name=host_info,json=hostInfo,proto3" json:"host_info,omitempty"`
        unknownFields     protoimpl.UnknownFields
        sizeCache         protoimpl.SizeCache
 }
@@ -73,6 +74,13 @@ func (x *CertificateRequest) GetPublicKey() []byte {
        return nil
 }
 
+func (x *CertificateRequest) GetHostInfo() *CertificateRequest_HostInfo {
+       if x != nil {
+               return x.HostInfo
+       }
+       return nil
+}
+
 type CertificatePreSignResponse struct {
        state         protoimpl.MessageState `protogen:"open.v1"`
        Digest        []byte                 `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"`
@@ -237,15 +245,71 @@ func (x *CertificateResponse) GetIntermediates() [][]byte {
        return nil
 }
 
+type CertificateRequest_HostInfo struct {
+       state         protoimpl.MessageState `protogen:"open.v1"`
+       Hostname      string                 `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
+       Domain        string                 `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *CertificateRequest_HostInfo) Reset() {
+       *x = CertificateRequest_HostInfo{}
+       mi := &file_mint_types_proto_msgTypes[4]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *CertificateRequest_HostInfo) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CertificateRequest_HostInfo) ProtoMessage() {}
+
+func (x *CertificateRequest_HostInfo) ProtoReflect() protoreflect.Message {
+       mi := &file_mint_types_proto_msgTypes[4]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use CertificateRequest_HostInfo.ProtoReflect.Descriptor instead.
+func (*CertificateRequest_HostInfo) Descriptor() ([]byte, []int) {
+       return file_mint_types_proto_rawDescGZIP(), []int{0, 0}
+}
+
+func (x *CertificateRequest_HostInfo) GetHostname() string {
+       if x != nil {
+               return x.Hostname
+       }
+       return ""
+}
+
+func (x *CertificateRequest_HostInfo) GetDomain() string {
+       if x != nil {
+               return x.Domain
+       }
+       return ""
+}
+
 var File_mint_types_proto protoreflect.FileDescriptor
 
 const file_mint_types_proto_rawDesc = "" +
        "\n" +
-       "\x10mint_types.proto\x12\x1afuhry.runtime.service.mint\"b\n" +
+       "\x10mint_types.proto\x12\x1afuhry.runtime.service.mint\"\xf8\x01\n" +
        "\x12CertificateRequest\x12-\n" +
        "\x12requested_identity\x18\x01 \x01(\tR\x11requestedIdentity\x12\x1d\n" +
        "\n" +
-       "public_key\x18\x02 \x01(\fR\tpublicKey\"H\n" +
+       "public_key\x18\x02 \x01(\fR\tpublicKey\x12T\n" +
+       "\thost_info\x18\x03 \x01(\v27.fuhry.runtime.service.mint.CertificateRequest.HostInfoR\bhostInfo\x1a>\n" +
+       "\bHostInfo\x12\x1a\n" +
+       "\bhostname\x18\x01 \x01(\tR\bhostname\x12\x16\n" +
+       "\x06domain\x18\x02 \x01(\tR\x06domain\"H\n" +
        "\x1aCertificatePreSignResponse\x12\x16\n" +
        "\x06digest\x18\x01 \x01(\fR\x06digest\x12\x12\n" +
        "\x04hash\x18\x02 \x01(\x05R\x04hash\"[\n" +
@@ -269,19 +333,21 @@ func file_mint_types_proto_rawDescGZIP() []byte {
        return file_mint_types_proto_rawDescData
 }
 
-var file_mint_types_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_mint_types_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
 var file_mint_types_proto_goTypes = []any{
-       (*CertificateRequest)(nil),         // 0: fuhry.runtime.service.mint.CertificateRequest
-       (*CertificatePreSignResponse)(nil), // 1: fuhry.runtime.service.mint.CertificatePreSignResponse
-       (*FinalizeRequest)(nil),            // 2: fuhry.runtime.service.mint.FinalizeRequest
-       (*CertificateResponse)(nil),        // 3: fuhry.runtime.service.mint.CertificateResponse
+       (*CertificateRequest)(nil),          // 0: fuhry.runtime.service.mint.CertificateRequest
+       (*CertificatePreSignResponse)(nil),  // 1: fuhry.runtime.service.mint.CertificatePreSignResponse
+       (*FinalizeRequest)(nil),             // 2: fuhry.runtime.service.mint.FinalizeRequest
+       (*CertificateResponse)(nil),         // 3: fuhry.runtime.service.mint.CertificateResponse
+       (*CertificateRequest_HostInfo)(nil), // 4: fuhry.runtime.service.mint.CertificateRequest.HostInfo
 }
 var file_mint_types_proto_depIdxs = []int32{
-       0, // [0:0] is the sub-list for method output_type
-       0, // [0:0] is the sub-list for method input_type
-       0, // [0:0] is the sub-list for extension type_name
-       0, // [0:0] is the sub-list for extension extendee
-       0, // [0:0] is the sub-list for field type_name
+       4, // 0: fuhry.runtime.service.mint.CertificateRequest.host_info:type_name -> fuhry.runtime.service.mint.CertificateRequest.HostInfo
+       1, // [1:1] is the sub-list for method output_type
+       1, // [1:1] is the sub-list for method input_type
+       1, // [1:1] is the sub-list for extension type_name
+       1, // [1:1] is the sub-list for extension extendee
+       0, // [0:1] is the sub-list for field type_name
 }
 
 func init() { file_mint_types_proto_init() }
@@ -295,7 +361,7 @@ func file_mint_types_proto_init() {
                        GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
                        RawDescriptor: unsafe.Slice(unsafe.StringData(file_mint_types_proto_rawDesc), len(file_mint_types_proto_rawDesc)),
                        NumEnums:      0,
-                       NumMessages:   4,
+                       NumMessages:   5,
                        NumExtensions: 0,
                        NumServices:   0,
                },
index 63c29f0044521ff348fb5d0eeb155e9e878ef18a..38ba34f4955984bddb934768f8972a9a08088ff2 100644 (file)
@@ -4,8 +4,13 @@ package fuhry.runtime.service.mint;
 option go_package = "go.fuhry.dev/runtime/proto/service/mint";
 
 message CertificateRequest {
+    message HostInfo {
+        string hostname = 1;
+        string domain = 2;
+    }
     string requested_identity = 1;
     bytes public_key = 2;
+    HostInfo host_info = 3;
 }
 
 message CertificatePreSignResponse {