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:
199
internal/webserver/client.go
Normal file
199
internal/webserver/client.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1"
|
||||
)
|
||||
|
||||
// VaultClient wraps the gRPC stubs for communicating with the vault.
|
||||
type VaultClient struct {
|
||||
conn *grpc.ClientConn
|
||||
system pb.SystemServiceClient
|
||||
auth pb.AuthServiceClient
|
||||
engine pb.EngineServiceClient
|
||||
pki pb.PKIServiceClient
|
||||
}
|
||||
|
||||
// NewVaultClient dials the vault gRPC server and returns a client.
|
||||
func NewVaultClient(addr, caCertPath string) (*VaultClient, error) {
|
||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
if caCertPath != "" {
|
||||
pemData, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: read CA cert: %w", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(pemData) {
|
||||
return nil, fmt.Errorf("webserver: parse CA cert")
|
||||
}
|
||||
tlsCfg.RootCAs = pool
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: dial vault: %w", err)
|
||||
}
|
||||
|
||||
return &VaultClient{
|
||||
conn: conn,
|
||||
system: pb.NewSystemServiceClient(conn),
|
||||
auth: pb.NewAuthServiceClient(conn),
|
||||
engine: pb.NewEngineServiceClient(conn),
|
||||
pki: pb.NewPKIServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying connection.
|
||||
func (c *VaultClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// withToken returns a context with the Bearer token in outgoing metadata.
|
||||
func withToken(ctx context.Context, token string) context.Context {
|
||||
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
// Status returns the current vault state string (e.g. "unsealed").
|
||||
func (c *VaultClient) Status(ctx context.Context) (string, error) {
|
||||
resp, err := c.system.Status(ctx, &pb.StatusRequest{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.State, nil
|
||||
}
|
||||
|
||||
// Init initializes the vault with the given password.
|
||||
func (c *VaultClient) Init(ctx context.Context, password string) error {
|
||||
_, err := c.system.Init(ctx, &pb.InitRequest{Password: password})
|
||||
return err
|
||||
}
|
||||
|
||||
// Unseal unseals the vault with the given password.
|
||||
func (c *VaultClient) Unseal(ctx context.Context, password string) error {
|
||||
_, err := c.system.Unseal(ctx, &pb.UnsealRequest{Password: password})
|
||||
return err
|
||||
}
|
||||
|
||||
// TokenInfo holds validated token details returned by the vault.
|
||||
type TokenInfo struct {
|
||||
Username string
|
||||
Roles []string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
// Login authenticates against the vault and returns the session token.
|
||||
func (c *VaultClient) Login(ctx context.Context, username, password, totpCode string) (string, error) {
|
||||
resp, err := c.auth.Login(ctx, &pb.LoginRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
TotpCode: totpCode,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Token, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a token against the vault and returns the token info.
|
||||
func (c *VaultClient) ValidateToken(ctx context.Context, token string) (*TokenInfo, error) {
|
||||
resp, err := c.auth.TokenInfo(withToken(ctx, token), &pb.TokenInfoRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TokenInfo{
|
||||
Username: resp.Username,
|
||||
Roles: resp.Roles,
|
||||
IsAdmin: resp.IsAdmin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MountInfo holds metadata about an engine mount.
|
||||
type MountInfo struct {
|
||||
Name string
|
||||
Type string
|
||||
MountPath string
|
||||
}
|
||||
|
||||
// ListMounts returns all engine mounts from the vault.
|
||||
func (c *VaultClient) ListMounts(ctx context.Context, token string) ([]MountInfo, error) {
|
||||
resp, err := c.engine.ListMounts(withToken(ctx, token), &pb.ListMountsRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mounts := make([]MountInfo, 0, len(resp.Mounts))
|
||||
for _, m := range resp.Mounts {
|
||||
mounts = append(mounts, MountInfo{
|
||||
Name: m.Name,
|
||||
Type: m.Type,
|
||||
MountPath: m.MountPath,
|
||||
})
|
||||
}
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
// Mount creates a new engine mount on the vault.
|
||||
func (c *VaultClient) Mount(ctx context.Context, token, name, engineType string, config map[string]interface{}) error {
|
||||
req := &pb.MountRequest{
|
||||
Name: name,
|
||||
Type: engineType,
|
||||
}
|
||||
if len(config) > 0 {
|
||||
s, err := structFromMap(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webserver: encode mount config: %w", err)
|
||||
}
|
||||
req.Config = s
|
||||
}
|
||||
_, err := c.engine.Mount(withToken(ctx, token), req)
|
||||
return err
|
||||
}
|
||||
|
||||
// EngineRequest sends a generic engine operation to the vault.
|
||||
func (c *VaultClient) EngineRequest(ctx context.Context, token, mount, operation string, data map[string]interface{}) (map[string]interface{}, error) {
|
||||
req := &pb.EngineRequest{
|
||||
Mount: mount,
|
||||
Operation: operation,
|
||||
}
|
||||
if len(data) > 0 {
|
||||
s, err := structFromMap(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: encode engine request: %w", err)
|
||||
}
|
||||
req.Data = s
|
||||
}
|
||||
resp, err := c.engine.Request(withToken(ctx, token), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return resp.Data.AsMap(), nil
|
||||
}
|
||||
|
||||
// GetRootCert returns the root CA certificate PEM for the given mount.
|
||||
func (c *VaultClient) GetRootCert(ctx context.Context, mount string) ([]byte, error) {
|
||||
resp, err := c.pki.GetRootCert(ctx, &pb.GetRootCertRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.CertPem, nil
|
||||
}
|
||||
|
||||
// GetIssuerCert returns a named issuer certificate PEM for the given mount.
|
||||
func (c *VaultClient) GetIssuerCert(ctx context.Context, mount, issuer string) ([]byte, error) {
|
||||
resp, err := c.pki.GetIssuerCert(ctx, &pb.GetIssuerCertRequest{Mount: mount, Issuer: issuer})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.CertPem, nil
|
||||
}
|
||||
Reference in New Issue
Block a user