Files
metacrypt/cmd/metacrypt-web/main.go
Kyle Isom cc1ac2e255 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>
2026-03-15 10:15:47 -07:00

76 lines
1.6 KiB
Go

// metacrypt-web is the standalone web UI server for Metacrypt.
// It communicates with the vault over gRPC and can be restarted independently
// without requiring the vault to be re-unsealed.
package main
import (
"context"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"git.wntrmute.dev/kyle/metacrypt/internal/config"
"git.wntrmute.dev/kyle/metacrypt/internal/webserver"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "metacrypt-web",
Short: "Metacrypt web UI server",
Long: "Standalone web UI server for Metacrypt. Communicates with the vault over gRPC.",
RunE: run,
}
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default /srv/metacrypt/metacrypt.toml)")
}
func run(cmd *cobra.Command, args []string) error {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
configPath := cfgFile
if configPath == "" {
configPath = "/srv/metacrypt/metacrypt.toml"
}
cfg, err := config.Load(configPath)
if err != nil {
return err
}
if cfg.Web.VaultGRPC == "" {
return fmt.Errorf("web.vault_grpc is required in config")
}
ws, err := webserver.New(cfg, logger)
if err != nil {
return err
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := ws.Start(); err != nil {
logger.Error("web server error", "error", err)
os.Exit(1)
}
}()
<-ctx.Done()
logger.Info("shutting down web server")
return ws.Shutdown(context.Background())
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}