Separate web UI into standalone metacrypt-web binary
The vault server holds in-memory unsealed state (KEK, engine keys) that is lost on restart, requiring a full unseal ceremony. Previously the web UI ran inside the vault process, so any UI change forced a restart and re-unseal. This change extracts the web UI into a separate metacrypt-web binary that communicates with the vault over an authenticated gRPC connection. The web server carries no sealed state and can be restarted freely. - gen/metacrypt/v1/: generated Go bindings from proto/metacrypt/v1/ - internal/grpcserver/: full gRPC server implementation (System, Auth, Engine, PKI, Policy, ACME services) with seal/auth/admin interceptors - internal/webserver/: web server with gRPC vault client; templates embedded via web/embed.go (no runtime web/ directory needed) - cmd/metacrypt-web/: standalone binary entry point - internal/config: added [web] section (listen_addr, vault_grpc, etc.) - internal/server/routes.go: removed all web UI routes and handlers - cmd/metacrypt/server.go: starts gRPC server alongside HTTP server - Deploy: Dockerfile builds both binaries, docker-compose adds metacrypt-web service, new metacrypt-web.service systemd unit, Makefile gains proto/metacrypt-web targets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
130
internal/grpcserver/acme.go
Normal file
130
internal/grpcserver/acme.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package grpcserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1"
|
||||
internacme "git.wntrmute.dev/kyle/metacrypt/internal/acme"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||
)
|
||||
|
||||
type acmeServer struct {
|
||||
pb.UnimplementedACMEServiceServer
|
||||
s *GRPCServer
|
||||
}
|
||||
|
||||
func (as *acmeServer) CreateEAB(ctx context.Context, req *pb.CreateEABRequest) (*pb.CreateEABResponse, error) {
|
||||
ti := tokenInfoFromContext(ctx)
|
||||
h, err := as.getOrCreateHandler(req.Mount)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, "mount not found")
|
||||
}
|
||||
cred, err := h.CreateEAB(ctx, ti.Username)
|
||||
if err != nil {
|
||||
as.s.logger.Error("grpc: acme create EAB", "error", err)
|
||||
return nil, status.Error(codes.Internal, "failed to create EAB credentials")
|
||||
}
|
||||
return &pb.CreateEABResponse{Kid: cred.KID, HmacKey: cred.HMACKey}, nil
|
||||
}
|
||||
|
||||
func (as *acmeServer) SetConfig(ctx context.Context, req *pb.SetACMEConfigRequest) (*pb.SetACMEConfigResponse, error) {
|
||||
if req.DefaultIssuer == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "default_issuer is required")
|
||||
}
|
||||
// Verify mount exists.
|
||||
if _, err := as.getOrCreateHandler(req.Mount); err != nil {
|
||||
return nil, status.Error(codes.NotFound, "mount not found")
|
||||
}
|
||||
cfg := &internacme.ACMEConfig{DefaultIssuer: req.DefaultIssuer}
|
||||
data, _ := json.Marshal(cfg)
|
||||
barrierPath := "acme/" + req.Mount + "/config.json"
|
||||
if err := as.s.sealMgr.Barrier().Put(ctx, barrierPath, data); err != nil {
|
||||
as.s.logger.Error("grpc: acme set config", "error", err)
|
||||
return nil, status.Error(codes.Internal, "failed to save config")
|
||||
}
|
||||
return &pb.SetACMEConfigResponse{Ok: true}, nil
|
||||
}
|
||||
|
||||
func (as *acmeServer) ListAccounts(ctx context.Context, req *pb.ListACMEAccountsRequest) (*pb.ListACMEAccountsResponse, error) {
|
||||
h, err := as.getOrCreateHandler(req.Mount)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, "mount not found")
|
||||
}
|
||||
accounts, err := h.ListAccounts(ctx)
|
||||
if err != nil {
|
||||
as.s.logger.Error("grpc: acme list accounts", "error", err)
|
||||
return nil, status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
pbAccounts := make([]*pb.ACMEAccount, 0, len(accounts))
|
||||
for _, a := range accounts {
|
||||
contacts := make([]string, len(a.Contact))
|
||||
copy(contacts, a.Contact)
|
||||
pbAccounts = append(pbAccounts, &pb.ACMEAccount{
|
||||
Id: a.ID,
|
||||
Status: a.Status,
|
||||
Contact: contacts,
|
||||
MciasUsername: a.MCIASUsername,
|
||||
CreatedAt: a.CreatedAt.String(),
|
||||
})
|
||||
}
|
||||
return &pb.ListACMEAccountsResponse{Accounts: pbAccounts}, nil
|
||||
}
|
||||
|
||||
func (as *acmeServer) ListOrders(ctx context.Context, req *pb.ListACMEOrdersRequest) (*pb.ListACMEOrdersResponse, error) {
|
||||
h, err := as.getOrCreateHandler(req.Mount)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, "mount not found")
|
||||
}
|
||||
orders, err := h.ListOrders(ctx)
|
||||
if err != nil {
|
||||
as.s.logger.Error("grpc: acme list orders", "error", err)
|
||||
return nil, status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
pbOrders := make([]*pb.ACMEOrder, 0, len(orders))
|
||||
for _, o := range orders {
|
||||
identifiers := make([]string, 0, len(o.Identifiers))
|
||||
for _, id := range o.Identifiers {
|
||||
identifiers = append(identifiers, id.Type+":"+id.Value)
|
||||
}
|
||||
pbOrders = append(pbOrders, &pb.ACMEOrder{
|
||||
Id: o.ID,
|
||||
AccountId: o.AccountID,
|
||||
Status: o.Status,
|
||||
Identifiers: identifiers,
|
||||
CreatedAt: o.CreatedAt.String(),
|
||||
ExpiresAt: o.ExpiresAt.String(),
|
||||
})
|
||||
}
|
||||
return &pb.ListACMEOrdersResponse{Orders: pbOrders}, nil
|
||||
}
|
||||
|
||||
func (as *acmeServer) getOrCreateHandler(mountName string) (*internacme.Handler, error) {
|
||||
as.s.mu.Lock()
|
||||
defer as.s.mu.Unlock()
|
||||
|
||||
// Verify mount is a CA engine.
|
||||
mount, err := as.s.engines.GetMount(mountName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mount.Type != engine.EngineTypeCA {
|
||||
return nil, engine.ErrMountNotFound
|
||||
}
|
||||
|
||||
// Check handler cache on GRPCServer.
|
||||
if h, ok := as.s.acmeHandlers[mountName]; ok {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
baseURL := as.s.cfg.Server.ExternalURL
|
||||
if baseURL == "" {
|
||||
baseURL = "https://" + as.s.cfg.Server.ListenAddr
|
||||
}
|
||||
h := internacme.NewHandler(mountName, as.s.sealMgr.Barrier(), as.s.engines, baseURL, as.s.logger)
|
||||
as.s.acmeHandlers[mountName] = h
|
||||
return h, nil
|
||||
}
|
||||
Reference in New Issue
Block a user