Files
metacrypt/internal/grpcserver/server.go
Kyle Isom 28d6f9fa1f Fix ListIssuers auth: move from public to auth-required methods
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>
2026-03-27 02:24:11 -07:00

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,
}
}