Log Info-level audit events on success for: - system: Init, Unseal, Seal - auth: Login, Logout - engine: Mount, Unmount - policy: CreatePolicy, DeletePolicy - ca: ImportRoot, CreateIssuer, DeleteIssuer, IssueCert, RenewCert Each log line includes relevant identifiers (mount, issuer, serial, CN, SANs, username) so that certificate issuance and other privileged operations are traceable in the server logs. Co-authored-by: Junie <junie@jetbrains.com>
317 lines
9.0 KiB
Go
317 lines
9.0 KiB
Go
package webserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
|
)
|
|
|
|
// 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
|
|
ca pb.CAServiceClient
|
|
}
|
|
|
|
// NewVaultClient dials the vault gRPC server and returns a client.
|
|
func NewVaultClient(addr, caCertPath string, logger *slog.Logger) (*VaultClient, error) {
|
|
logger.Debug("connecting to vault", "addr", addr, "ca_cert", caCertPath)
|
|
|
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
|
if caCertPath != "" {
|
|
logger.Debug("loading vault CA certificate", "path", 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
|
|
logger.Debug("vault CA certificate loaded successfully")
|
|
} else {
|
|
logger.Debug("no CA cert configured, using system roots")
|
|
}
|
|
|
|
logger.Debug("dialing vault gRPC", "addr", addr)
|
|
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("webserver: dial vault: %w", err)
|
|
}
|
|
logger.Debug("vault gRPC connection established", "addr", addr)
|
|
|
|
return &VaultClient{
|
|
conn: conn,
|
|
system: pb.NewSystemServiceClient(conn),
|
|
auth: pb.NewAuthServiceClient(conn),
|
|
engine: pb.NewEngineServiceClient(conn),
|
|
pki: pb.NewPKIServiceClient(conn),
|
|
ca: pb.NewCAServiceClient(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 {
|
|
cfg := make(map[string]string, len(config))
|
|
for k, v := range config {
|
|
cfg[k] = fmt.Sprintf("%v", v)
|
|
}
|
|
req.Config = cfg
|
|
}
|
|
_, err := c.engine.Mount(withToken(ctx, token), req)
|
|
return err
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ImportRoot imports an existing root CA certificate and key into the given mount.
|
|
func (c *VaultClient) ImportRoot(ctx context.Context, token, mount, certPEM, keyPEM string) error {
|
|
_, err := c.ca.ImportRoot(withToken(ctx, token), &pb.ImportRootRequest{
|
|
Mount: mount,
|
|
CertPem: []byte(certPEM),
|
|
KeyPem: []byte(keyPEM),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// CreateIssuerRequest holds parameters for creating an intermediate CA issuer.
|
|
type CreateIssuerRequest struct {
|
|
Mount string
|
|
Name string
|
|
KeyAlgorithm string
|
|
KeySize int32
|
|
Expiry string
|
|
MaxTTL string
|
|
}
|
|
|
|
// CreateIssuer creates a new intermediate CA issuer on the given mount.
|
|
func (c *VaultClient) CreateIssuer(ctx context.Context, token string, req CreateIssuerRequest) error {
|
|
_, err := c.ca.CreateIssuer(withToken(ctx, token), &pb.CreateIssuerRequest{
|
|
Mount: req.Mount,
|
|
Name: req.Name,
|
|
KeyAlgorithm: req.KeyAlgorithm,
|
|
KeySize: req.KeySize,
|
|
Expiry: req.Expiry,
|
|
MaxTtl: req.MaxTTL,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// ListIssuers returns the names of all issuers for the given mount.
|
|
func (c *VaultClient) ListIssuers(ctx context.Context, token, mount string) ([]string, error) {
|
|
resp, err := c.ca.ListIssuers(withToken(ctx, token), &pb.ListIssuersRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Issuers, nil
|
|
}
|
|
|
|
// IssueCertRequest holds parameters for issuing a leaf certificate.
|
|
type IssueCertRequest struct {
|
|
Mount string
|
|
Issuer string
|
|
Profile string
|
|
CommonName string
|
|
DNSNames []string
|
|
IPAddresses []string
|
|
TTL string
|
|
KeyUsages []string
|
|
ExtKeyUsages []string
|
|
}
|
|
|
|
// IssuedCert holds the result of a certificate issuance.
|
|
type IssuedCert struct {
|
|
Serial string
|
|
CertPEM string
|
|
KeyPEM string
|
|
ChainPEM string
|
|
ExpiresAt string
|
|
}
|
|
|
|
// IssueCert issues a new leaf certificate from the named issuer.
|
|
func (c *VaultClient) IssueCert(ctx context.Context, token string, req IssueCertRequest) (*IssuedCert, error) {
|
|
resp, err := c.ca.IssueCert(withToken(ctx, token), &pb.IssueCertRequest{
|
|
Mount: req.Mount,
|
|
Issuer: req.Issuer,
|
|
Profile: req.Profile,
|
|
CommonName: req.CommonName,
|
|
DnsNames: req.DNSNames,
|
|
IpAddresses: req.IPAddresses,
|
|
Ttl: req.TTL,
|
|
KeyUsages: req.KeyUsages,
|
|
ExtKeyUsages: req.ExtKeyUsages,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
issued := &IssuedCert{
|
|
Serial: resp.Serial,
|
|
CertPEM: string(resp.CertPem),
|
|
KeyPEM: string(resp.KeyPem),
|
|
ChainPEM: string(resp.ChainPem),
|
|
}
|
|
if resp.ExpiresAt != nil {
|
|
issued.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return issued, nil
|
|
}
|
|
|
|
// CertSummary holds lightweight certificate metadata for list views.
|
|
type CertSummary struct {
|
|
Serial string
|
|
Issuer string
|
|
CommonName string
|
|
Profile string
|
|
IssuedBy string
|
|
IssuedAt string
|
|
ExpiresAt string
|
|
}
|
|
|
|
// ListCerts returns all certificate summaries for the given mount.
|
|
func (c *VaultClient) ListCerts(ctx context.Context, token, mount string) ([]CertSummary, error) {
|
|
resp, err := c.ca.ListCerts(withToken(ctx, token), &pb.ListCertsRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs := make([]CertSummary, 0, len(resp.Certs))
|
|
for _, s := range resp.Certs {
|
|
cs := CertSummary{
|
|
Serial: s.Serial,
|
|
Issuer: s.Issuer,
|
|
CommonName: s.CommonName,
|
|
Profile: s.Profile,
|
|
IssuedBy: s.IssuedBy,
|
|
}
|
|
if s.IssuedAt != nil {
|
|
cs.IssuedAt = s.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if s.ExpiresAt != nil {
|
|
cs.ExpiresAt = s.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
certs = append(certs, cs)
|
|
}
|
|
return certs, nil
|
|
}
|