Add TLS unsealing via gRPC to CLI and server
Implements the SystemService gRPC endpoint (Status, Init, Unseal, Seal) alongside the existing REST API, secured with the same TLS certificate. The `metacrypt unseal` CLI command now prefers gRPC when --grpc-addr is provided, falling back to the REST API via --addr. Both transports require TLS; a custom CA certificate can be supplied with --ca-cert. Server changes: - internal/server/grpc.go: SystemServiceServer implementation with StartGRPC/ShutdownGRPC methods; uses the TLS cert from config. - internal/server/server.go: adds grpcSrv field and grpc import. - cmd/metacrypt/server.go: starts gRPC goroutine when grpc_addr is set in config, shuts it down on signal. Generated code (from proto/metacrypt/v1/system.proto): - gen/metacrypt/v1/system.pb.go: protobuf message types - gen/metacrypt/v1/system_grpc.pb.go: gRPC client/server stubs Dependencies added to go.mod (run `go mod tidy` to populate go.sum): - google.golang.org/grpc v1.71.1 - google.golang.org/protobuf v1.36.5 - google.golang.org/genproto/googleapis/rpc (indirect) - golang.org/x/net (indirect) https://claude.ai/code/session_013m1QXGoTB4jaPUN5gwir8F
This commit is contained in:
@@ -86,7 +86,17 @@ func runServer(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if cfg.Server.GRPCAddr != "" {
|
||||
go func() {
|
||||
if err := srv.StartGRPC(); err != nil {
|
||||
logger.Error("grpc server error", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
logger.Info("shutting down")
|
||||
srv.ShutdownGRPC()
|
||||
return srv.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
132
cmd/metacrypt/unseal.go
Normal file
132
cmd/metacrypt/unseal.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"golang.org/x/term"
|
||||
|
||||
metacryptv1 "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1"
|
||||
)
|
||||
|
||||
var unsealCmd = &cobra.Command{
|
||||
Use: "unseal",
|
||||
Short: "Unseal the Metacrypt service",
|
||||
Long: "Send an unseal request to a running Metacrypt server over TLS. Uses gRPC when --grpc-addr is provided, otherwise falls back to the REST API.",
|
||||
RunE: runUnseal,
|
||||
}
|
||||
|
||||
var (
|
||||
unsealAddr string
|
||||
unsealGRPCAddr string
|
||||
unsealCACert string
|
||||
)
|
||||
|
||||
func init() {
|
||||
unsealCmd.Flags().StringVar(&unsealAddr, "addr", "", "REST server address (e.g., https://localhost:8443)")
|
||||
unsealCmd.Flags().StringVar(&unsealGRPCAddr, "grpc-addr", "", "gRPC server address (e.g., localhost:9443); preferred over --addr when set")
|
||||
unsealCmd.Flags().StringVar(&unsealCACert, "ca-cert", "", "path to CA certificate for TLS verification")
|
||||
rootCmd.AddCommand(unsealCmd)
|
||||
}
|
||||
|
||||
func runUnseal(cmd *cobra.Command, args []string) error {
|
||||
if unsealGRPCAddr == "" && unsealAddr == "" {
|
||||
return fmt.Errorf("one of --grpc-addr or --addr is required")
|
||||
}
|
||||
|
||||
fmt.Print("Unseal password: ")
|
||||
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read password: %w", err)
|
||||
}
|
||||
|
||||
if unsealGRPCAddr != "" {
|
||||
return unsealViaGRPC(unsealGRPCAddr, unsealCACert, string(passwordBytes))
|
||||
}
|
||||
return unsealViaREST(unsealAddr, unsealCACert, string(passwordBytes))
|
||||
}
|
||||
|
||||
func buildTLSConfig(caCertPath string) (*tls.Config, error) {
|
||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
if caCertPath != "" {
|
||||
pem, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CA cert: %w", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(pem) {
|
||||
return nil, fmt.Errorf("no valid certs in CA file")
|
||||
}
|
||||
tlsCfg.RootCAs = pool
|
||||
}
|
||||
return tlsCfg, nil
|
||||
}
|
||||
|
||||
func unsealViaGRPC(addr, caCertPath, password string) error {
|
||||
tlsCfg, err := buildTLSConfig(caCertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("grpc dial: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := metacryptv1.NewSystemServiceClient(conn)
|
||||
resp, err := client.Unseal(context.Background(), &metacryptv1.UnsealRequest{Password: password})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unseal failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("State: %s\n", resp.State)
|
||||
return nil
|
||||
}
|
||||
|
||||
func unsealViaREST(addr, caCertPath, password string) error {
|
||||
tlsCfg, err := buildTLSConfig(caCertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: tlsCfg},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(map[string]string{"password": password})
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Post(addr+"/v1/unseal", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
State string `json:"state"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
if result.Error != "" {
|
||||
return fmt.Errorf("unseal failed: %s", result.Error)
|
||||
}
|
||||
|
||||
fmt.Printf("State: %s\n", result.State)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user