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) //nolint:gosec 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 }