ListIssuers was miscategorized as a public gRPC method, but the CA engine handler requires CallerInfo with user role. When called without auth (public path), the interceptor skipped token validation, so CallerInfo was nil and the handler returned ErrUnauthorized — which the web UI silently swallowed, showing "No issuers configured." Security: gRPC interceptor map correction (ListIssuers requires auth) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
339 lines
13 KiB
Go
339 lines
13 KiB
Go
// Package grpcserver implements the gRPC server for Metacrypt.
|
|
package grpcserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
pb "git.wntrmute.dev/mc/metacrypt/gen/metacrypt/v2"
|
|
"git.wntrmute.dev/mc/mcdsl/grpcserver"
|
|
|
|
internacme "git.wntrmute.dev/mc/metacrypt/internal/acme"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/audit"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/auth"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/config"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/engine"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/policy"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/seal"
|
|
)
|
|
|
|
// GRPCServer wraps the mcdsl gRPC server and all service implementations.
|
|
type GRPCServer struct {
|
|
cfg *config.Config
|
|
sealMgr *seal.Manager
|
|
auth *auth.Authenticator
|
|
policy *policy.Engine
|
|
engines *engine.Registry
|
|
audit *audit.Logger
|
|
logger *slog.Logger
|
|
srv *grpcserver.Server
|
|
acmeHandlers map[string]*internacme.Handler
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// New creates a new GRPCServer.
|
|
func New(cfg *config.Config, sealMgr *seal.Manager, authenticator *auth.Authenticator,
|
|
policyEngine *policy.Engine, engineRegistry *engine.Registry, auditLog *audit.Logger, logger *slog.Logger) *GRPCServer {
|
|
return &GRPCServer{
|
|
cfg: cfg,
|
|
sealMgr: sealMgr,
|
|
auth: authenticator,
|
|
policy: policyEngine,
|
|
engines: engineRegistry,
|
|
audit: auditLog,
|
|
logger: logger,
|
|
acmeHandlers: make(map[string]*internacme.Handler),
|
|
}
|
|
}
|
|
|
|
// Start starts the gRPC server on cfg.Server.GRPCAddr.
|
|
func (s *GRPCServer) Start() error {
|
|
if s.cfg.Server.GRPCAddr == "" {
|
|
s.logger.Info("grpc_addr not configured, gRPC server disabled")
|
|
return nil
|
|
}
|
|
|
|
opts := &grpcserver.Options{
|
|
PreInterceptors: []grpc.UnaryServerInterceptor{sealInterceptor(s.sealMgr, s.logger, sealRequiredMethods())},
|
|
PostInterceptors: []grpc.UnaryServerInterceptor{auditInterceptor(s.audit)},
|
|
}
|
|
|
|
srv, err := grpcserver.New(
|
|
s.cfg.Server.TLSCert,
|
|
s.cfg.Server.TLSKey,
|
|
s.auth,
|
|
methodMap(),
|
|
s.logger,
|
|
opts,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("grpc: create server: %w", err)
|
|
}
|
|
s.srv = srv
|
|
|
|
pb.RegisterSystemServiceServer(srv.GRPCServer, &systemServer{s: s})
|
|
pb.RegisterAuthServiceServer(srv.GRPCServer, &authServer{s: s})
|
|
pb.RegisterEngineServiceServer(srv.GRPCServer, &engineServer{s: s})
|
|
pb.RegisterPKIServiceServer(srv.GRPCServer, &pkiServer{s: s})
|
|
pb.RegisterCAServiceServer(srv.GRPCServer, &caServer{s: s})
|
|
pb.RegisterPolicyServiceServer(srv.GRPCServer, &policyServer{s: s})
|
|
pb.RegisterBarrierServiceServer(srv.GRPCServer, &barrierServer{s: s})
|
|
pb.RegisterUserServiceServer(srv.GRPCServer, &userServer{s: s})
|
|
pb.RegisterACMEServiceServer(srv.GRPCServer, &acmeServer{s: s})
|
|
pb.RegisterSSHCAServiceServer(srv.GRPCServer, &sshcaServer{s: s})
|
|
pb.RegisterTransitServiceServer(srv.GRPCServer, &transitServer{s: s})
|
|
|
|
return srv.Serve(s.cfg.Server.GRPCAddr)
|
|
}
|
|
|
|
// Shutdown gracefully stops the gRPC server.
|
|
func (s *GRPCServer) Shutdown() {
|
|
if s.srv != nil {
|
|
s.srv.Stop()
|
|
}
|
|
}
|
|
|
|
// extractToken extracts the bearer token from gRPC metadata. Used by the
|
|
// auth service Logout handler which needs the raw token.
|
|
func extractToken(ctx context.Context) string {
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
vals := md.Get("authorization")
|
|
if len(vals) == 0 {
|
|
return ""
|
|
}
|
|
v := vals[0]
|
|
if strings.HasPrefix(v, "Bearer ") {
|
|
return strings.TrimPrefix(v, "Bearer ")
|
|
}
|
|
return v
|
|
}
|
|
|
|
// callerUsername returns the username from the token info in the context,
|
|
// or empty string if no token info is present.
|
|
func callerUsername(ctx context.Context) string {
|
|
ti := auth.TokenInfoFromContext(ctx)
|
|
if ti == nil {
|
|
return ""
|
|
}
|
|
return ti.Username
|
|
}
|
|
|
|
// methodMap builds the mcdsl grpcserver.MethodMap for Metacrypt.
|
|
//
|
|
// Public methods require no authentication. Auth-required and admin-required
|
|
// methods are enforced by the mcdsl auth interceptor.
|
|
func methodMap() grpcserver.MethodMap {
|
|
return grpcserver.MethodMap{
|
|
Public: publicMethods(),
|
|
|
|
AuthRequired: authRequiredMethods(),
|
|
|
|
AdminRequired: adminRequiredMethods(),
|
|
}
|
|
}
|
|
|
|
// publicMethods returns methods that require no authentication.
|
|
// These include system lifecycle RPCs (status, init, unseal), the login
|
|
// endpoint, and read-only PKI/CA/SSH CA endpoints that serve public
|
|
// certificates and keys.
|
|
func publicMethods() map[string]bool {
|
|
return map[string]bool{
|
|
// System lifecycle — always available.
|
|
"/metacrypt.v2.SystemService/Status": true,
|
|
"/metacrypt.v2.SystemService/Init": true,
|
|
"/metacrypt.v2.SystemService/Unseal": true,
|
|
// Auth — login requires no existing token.
|
|
"/metacrypt.v2.AuthService/Login": true,
|
|
// PKI read-only — public certificates.
|
|
"/metacrypt.v2.PKIService/GetRootCert": true,
|
|
"/metacrypt.v2.PKIService/GetChain": true,
|
|
"/metacrypt.v2.PKIService/GetIssuerCert": true,
|
|
// CA read-only — public certificates and chains.
|
|
"/metacrypt.v2.CAService/GetRoot": true,
|
|
"/metacrypt.v2.CAService/GetIssuer": true,
|
|
"/metacrypt.v2.CAService/GetChain": true,
|
|
// SSH CA — public key and key revocation list.
|
|
"/metacrypt.v2.SSHCAService/GetCAPublicKey": true,
|
|
"/metacrypt.v2.SSHCAService/GetKRL": true,
|
|
}
|
|
}
|
|
|
|
// authRequiredMethods returns methods that require a valid MCIAS token but
|
|
// not necessarily the admin role.
|
|
func authRequiredMethods() map[string]bool {
|
|
return map[string]bool{
|
|
"/metacrypt.v2.AuthService/Logout": true,
|
|
"/metacrypt.v2.AuthService/TokenInfo": true,
|
|
"/metacrypt.v2.EngineService/ListMounts": true,
|
|
"/metacrypt.v2.CAService/ListIssuers": true,
|
|
"/metacrypt.v2.CAService/IssueCert": true,
|
|
"/metacrypt.v2.CAService/GetCert": true,
|
|
"/metacrypt.v2.CAService/ListCerts": true,
|
|
"/metacrypt.v2.CAService/RenewCert": true,
|
|
"/metacrypt.v2.CAService/SignCSR": true,
|
|
"/metacrypt.v2.UserService/Register": true,
|
|
"/metacrypt.v2.UserService/GetPublicKey": true,
|
|
"/metacrypt.v2.UserService/ListUsers": true,
|
|
"/metacrypt.v2.UserService/Encrypt": true,
|
|
"/metacrypt.v2.UserService/Decrypt": true,
|
|
"/metacrypt.v2.UserService/ReEncrypt": true,
|
|
"/metacrypt.v2.UserService/RotateKey": true,
|
|
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
|
// SSH CA — sign and read operations.
|
|
"/metacrypt.v2.SSHCAService/SignHost": true,
|
|
"/metacrypt.v2.SSHCAService/SignUser": true,
|
|
"/metacrypt.v2.SSHCAService/GetProfile": true,
|
|
"/metacrypt.v2.SSHCAService/ListProfiles": true,
|
|
"/metacrypt.v2.SSHCAService/GetCert": true,
|
|
"/metacrypt.v2.SSHCAService/ListCerts": true,
|
|
// Transit — data-plane operations.
|
|
"/metacrypt.v2.TransitService/GetKey": true,
|
|
"/metacrypt.v2.TransitService/ListKeys": true,
|
|
"/metacrypt.v2.TransitService/Encrypt": true,
|
|
"/metacrypt.v2.TransitService/Decrypt": true,
|
|
"/metacrypt.v2.TransitService/Rewrap": true,
|
|
"/metacrypt.v2.TransitService/BatchEncrypt": true,
|
|
"/metacrypt.v2.TransitService/BatchDecrypt": true,
|
|
"/metacrypt.v2.TransitService/BatchRewrap": true,
|
|
"/metacrypt.v2.TransitService/Sign": true,
|
|
"/metacrypt.v2.TransitService/Verify": true,
|
|
"/metacrypt.v2.TransitService/Hmac": true,
|
|
"/metacrypt.v2.TransitService/GetPublicKey": true,
|
|
}
|
|
}
|
|
|
|
// adminRequiredMethods returns methods that require a valid MCIAS token
|
|
// with the admin role.
|
|
func adminRequiredMethods() map[string]bool {
|
|
return map[string]bool{
|
|
"/metacrypt.v2.SystemService/Seal": true,
|
|
"/metacrypt.v2.EngineService/Mount": true,
|
|
"/metacrypt.v2.EngineService/Unmount": true,
|
|
"/metacrypt.v2.CAService/ImportRoot": true,
|
|
"/metacrypt.v2.CAService/CreateIssuer": true,
|
|
"/metacrypt.v2.CAService/DeleteIssuer": true,
|
|
"/metacrypt.v2.CAService/RevokeCert": true,
|
|
"/metacrypt.v2.CAService/DeleteCert": true,
|
|
"/metacrypt.v2.PolicyService/CreatePolicy": true,
|
|
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
|
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
|
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
|
// User.
|
|
"/metacrypt.v2.UserService/Provision": true,
|
|
"/metacrypt.v2.UserService/DeleteUser": true,
|
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
|
"/metacrypt.v2.ACMEService/ListOrders": true,
|
|
"/metacrypt.v2.BarrierService/ListKeys": true,
|
|
"/metacrypt.v2.BarrierService/RotateMEK": true,
|
|
"/metacrypt.v2.BarrierService/RotateKey": true,
|
|
"/metacrypt.v2.BarrierService/Migrate": true,
|
|
// SSH CA.
|
|
"/metacrypt.v2.SSHCAService/CreateProfile": true,
|
|
"/metacrypt.v2.SSHCAService/UpdateProfile": true,
|
|
"/metacrypt.v2.SSHCAService/DeleteProfile": true,
|
|
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
|
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
|
// Transit.
|
|
"/metacrypt.v2.TransitService/CreateKey": true,
|
|
"/metacrypt.v2.TransitService/DeleteKey": true,
|
|
"/metacrypt.v2.TransitService/RotateKey": true,
|
|
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
|
"/metacrypt.v2.TransitService/TrimKey": true,
|
|
}
|
|
}
|
|
|
|
// sealRequiredMethods returns the set of RPC full names that require the vault
|
|
// to be unsealed. This is used by the sealInterceptor PreInterceptor.
|
|
func sealRequiredMethods() map[string]bool {
|
|
return map[string]bool{
|
|
"/metacrypt.v2.AuthService/Login": true,
|
|
"/metacrypt.v2.AuthService/Logout": true,
|
|
"/metacrypt.v2.AuthService/TokenInfo": true,
|
|
"/metacrypt.v2.EngineService/Mount": true,
|
|
"/metacrypt.v2.EngineService/Unmount": true,
|
|
"/metacrypt.v2.EngineService/ListMounts": true,
|
|
"/metacrypt.v2.PKIService/GetRootCert": true,
|
|
"/metacrypt.v2.PKIService/GetChain": true,
|
|
"/metacrypt.v2.PKIService/GetIssuerCert": true,
|
|
"/metacrypt.v2.CAService/ImportRoot": true,
|
|
"/metacrypt.v2.CAService/GetRoot": true,
|
|
"/metacrypt.v2.CAService/CreateIssuer": true,
|
|
"/metacrypt.v2.CAService/DeleteIssuer": true,
|
|
"/metacrypt.v2.CAService/ListIssuers": true,
|
|
"/metacrypt.v2.CAService/GetIssuer": true,
|
|
"/metacrypt.v2.CAService/GetChain": true,
|
|
"/metacrypt.v2.CAService/IssueCert": true,
|
|
"/metacrypt.v2.CAService/GetCert": true,
|
|
"/metacrypt.v2.CAService/ListCerts": true,
|
|
"/metacrypt.v2.CAService/RenewCert": true,
|
|
"/metacrypt.v2.CAService/SignCSR": true,
|
|
"/metacrypt.v2.CAService/RevokeCert": true,
|
|
"/metacrypt.v2.CAService/DeleteCert": true,
|
|
"/metacrypt.v2.PolicyService/CreatePolicy": true,
|
|
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
|
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
|
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
|
"/metacrypt.v2.UserService/Register": true,
|
|
"/metacrypt.v2.UserService/Provision": true,
|
|
"/metacrypt.v2.UserService/GetPublicKey": true,
|
|
"/metacrypt.v2.UserService/ListUsers": true,
|
|
"/metacrypt.v2.UserService/Encrypt": true,
|
|
"/metacrypt.v2.UserService/Decrypt": true,
|
|
"/metacrypt.v2.UserService/ReEncrypt": true,
|
|
"/metacrypt.v2.UserService/RotateKey": true,
|
|
"/metacrypt.v2.UserService/DeleteUser": true,
|
|
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
|
"/metacrypt.v2.ACMEService/ListOrders": true,
|
|
"/metacrypt.v2.BarrierService/ListKeys": true,
|
|
"/metacrypt.v2.BarrierService/RotateMEK": true,
|
|
"/metacrypt.v2.BarrierService/RotateKey": true,
|
|
"/metacrypt.v2.BarrierService/Migrate": true,
|
|
// SSH CA.
|
|
"/metacrypt.v2.SSHCAService/GetCAPublicKey": true,
|
|
"/metacrypt.v2.SSHCAService/SignHost": true,
|
|
"/metacrypt.v2.SSHCAService/SignUser": true,
|
|
"/metacrypt.v2.SSHCAService/CreateProfile": true,
|
|
"/metacrypt.v2.SSHCAService/UpdateProfile": true,
|
|
"/metacrypt.v2.SSHCAService/GetProfile": true,
|
|
"/metacrypt.v2.SSHCAService/ListProfiles": true,
|
|
"/metacrypt.v2.SSHCAService/DeleteProfile": true,
|
|
"/metacrypt.v2.SSHCAService/GetCert": true,
|
|
"/metacrypt.v2.SSHCAService/ListCerts": true,
|
|
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
|
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
|
"/metacrypt.v2.SSHCAService/GetKRL": true,
|
|
// Transit.
|
|
"/metacrypt.v2.TransitService/CreateKey": true,
|
|
"/metacrypt.v2.TransitService/DeleteKey": true,
|
|
"/metacrypt.v2.TransitService/GetKey": true,
|
|
"/metacrypt.v2.TransitService/ListKeys": true,
|
|
"/metacrypt.v2.TransitService/RotateKey": true,
|
|
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
|
"/metacrypt.v2.TransitService/TrimKey": true,
|
|
"/metacrypt.v2.TransitService/Encrypt": true,
|
|
"/metacrypt.v2.TransitService/Decrypt": true,
|
|
"/metacrypt.v2.TransitService/Rewrap": true,
|
|
"/metacrypt.v2.TransitService/BatchEncrypt": true,
|
|
"/metacrypt.v2.TransitService/BatchDecrypt": true,
|
|
"/metacrypt.v2.TransitService/BatchRewrap": true,
|
|
"/metacrypt.v2.TransitService/Sign": true,
|
|
"/metacrypt.v2.TransitService/Verify": true,
|
|
"/metacrypt.v2.TransitService/Hmac": true,
|
|
"/metacrypt.v2.TransitService/GetPublicKey": true,
|
|
// Seal itself requires unsealed state.
|
|
"/metacrypt.v2.SystemService/Seal": true,
|
|
}
|
|
}
|