The web UI connects to the vault API via gRPC using the Docker compose service name (e.g., "metacrypt:9443"), but the vault's TLS certificate has SANs for "crypt.metacircular.net" and "localhost". The new vault_sni config field overrides the TLS ServerName so certificate verification succeeds despite the hostname mismatch. Also updates metacrypt-rift.toml with vault_sni and temporarily binds the web UI port to 0.0.0.0 for direct access until mc-proxy is deployed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1119 lines
33 KiB
Go
1119 lines
33 KiB
Go
package webserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"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
|
|
policy pb.PolicyServiceClient
|
|
sshca pb.SSHCAServiceClient
|
|
transit pb.TransitServiceClient
|
|
user pb.UserServiceClient
|
|
}
|
|
|
|
// NewVaultClient dials the vault gRPC server and returns a client.
|
|
// NewVaultClient creates a gRPC client to the metacrypt vault API server.
|
|
// If sni is non-empty, it overrides the TLS server name for certificate
|
|
// verification (use when the dial address doesn't match a cert SAN).
|
|
func NewVaultClient(addr, caCertPath, sni string, logger *slog.Logger) (*VaultClient, error) {
|
|
logger.Debug("connecting to vault", "addr", addr, "ca_cert", caCertPath)
|
|
|
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13} //nolint:gosec // TLS 1.3 minimum
|
|
if sni != "" {
|
|
tlsCfg.ServerName = sni
|
|
}
|
|
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),
|
|
policy: pb.NewPolicyServiceClient(conn),
|
|
sshca: pb.NewSSHCAServiceClient(conn),
|
|
transit: pb.NewTransitServiceClient(conn),
|
|
user: pb.NewUserServiceClient(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
|
|
}
|
|
|
|
// SignCSRRequest holds parameters for signing an external CSR.
|
|
type SignCSRRequest struct {
|
|
Mount string
|
|
Issuer string
|
|
CSRPEM string
|
|
Profile string
|
|
TTL string
|
|
}
|
|
|
|
// SignedCert holds the result of signing a CSR.
|
|
type SignedCert struct {
|
|
Serial string
|
|
CertPEM string
|
|
ChainPEM string
|
|
ExpiresAt string
|
|
}
|
|
|
|
// SignCSR signs an externally generated CSR with the named issuer.
|
|
func (c *VaultClient) SignCSR(ctx context.Context, token string, req SignCSRRequest) (*SignedCert, error) {
|
|
resp, err := c.ca.SignCSR(withToken(ctx, token), &pb.SignCSRRequest{
|
|
Mount: req.Mount,
|
|
Issuer: req.Issuer,
|
|
CsrPem: []byte(req.CSRPEM),
|
|
Profile: req.Profile,
|
|
Ttl: req.TTL,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sc := &SignedCert{
|
|
Serial: resp.Serial,
|
|
CertPEM: string(resp.CertPem),
|
|
ChainPEM: string(resp.ChainPem),
|
|
}
|
|
if resp.ExpiresAt != nil {
|
|
sc.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return sc, nil
|
|
}
|
|
|
|
// CertDetail holds the full certificate record for the detail view.
|
|
type CertDetail struct {
|
|
Serial string
|
|
Issuer string
|
|
CommonName string
|
|
SANs []string
|
|
Profile string
|
|
IssuedBy string
|
|
IssuedAt string
|
|
ExpiresAt string
|
|
CertPEM string
|
|
Revoked bool
|
|
RevokedAt string
|
|
RevokedBy string
|
|
}
|
|
|
|
// GetCert retrieves a full certificate record by serial number.
|
|
func (c *VaultClient) GetCert(ctx context.Context, token, mount, serial string) (*CertDetail, error) {
|
|
resp, err := c.ca.GetCert(withToken(ctx, token), &pb.GetCertRequest{Mount: mount, Serial: serial})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rec := resp.GetCert()
|
|
if rec == nil {
|
|
return nil, fmt.Errorf("cert not found")
|
|
}
|
|
cd := &CertDetail{
|
|
Serial: rec.Serial,
|
|
Issuer: rec.Issuer,
|
|
CommonName: rec.CommonName,
|
|
SANs: rec.Sans,
|
|
Profile: rec.Profile,
|
|
IssuedBy: rec.IssuedBy,
|
|
CertPEM: string(rec.CertPem),
|
|
Revoked: rec.Revoked,
|
|
RevokedBy: rec.RevokedBy,
|
|
}
|
|
if rec.IssuedAt != nil {
|
|
cd.IssuedAt = rec.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if rec.ExpiresAt != nil {
|
|
cd.ExpiresAt = rec.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if rec.RevokedAt != nil {
|
|
cd.RevokedAt = rec.RevokedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return cd, nil
|
|
}
|
|
|
|
// RevokeCert marks a certificate as revoked.
|
|
func (c *VaultClient) RevokeCert(ctx context.Context, token, mount, serial string) error {
|
|
_, err := c.ca.RevokeCert(withToken(ctx, token), &pb.RevokeCertRequest{Mount: mount, Serial: serial})
|
|
return err
|
|
}
|
|
|
|
// DeleteCert permanently removes a certificate record.
|
|
func (c *VaultClient) DeleteCert(ctx context.Context, token, mount, serial string) error {
|
|
_, err := c.ca.DeleteCert(withToken(ctx, token), &pb.DeleteCertRequest{Mount: mount, Serial: serial})
|
|
return err
|
|
}
|
|
|
|
// PolicyRule holds a policy rule for display and management.
|
|
type PolicyRule struct {
|
|
ID string
|
|
Priority int
|
|
Effect string
|
|
Usernames []string
|
|
Roles []string
|
|
Resources []string
|
|
Actions []string
|
|
}
|
|
|
|
// ListPolicies returns all policy rules from the vault.
|
|
func (c *VaultClient) ListPolicies(ctx context.Context, token string) ([]PolicyRule, error) {
|
|
resp, err := c.policy.ListPolicies(withToken(ctx, token), &pb.ListPoliciesRequest{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rules := make([]PolicyRule, 0, len(resp.Rules))
|
|
for _, r := range resp.Rules {
|
|
rules = append(rules, pbToRule(r))
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// GetPolicy retrieves a single policy rule by ID.
|
|
func (c *VaultClient) GetPolicy(ctx context.Context, token, id string) (*PolicyRule, error) {
|
|
resp, err := c.policy.GetPolicy(withToken(ctx, token), &pb.GetPolicyRequest{Id: id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rule := pbToRule(resp.Rule)
|
|
return &rule, nil
|
|
}
|
|
|
|
// CreatePolicy creates a new policy rule.
|
|
func (c *VaultClient) CreatePolicy(ctx context.Context, token string, rule PolicyRule) (*PolicyRule, error) {
|
|
resp, err := c.policy.CreatePolicy(withToken(ctx, token), &pb.CreatePolicyRequest{
|
|
Rule: ruleToPB(rule),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
created := pbToRule(resp.Rule)
|
|
return &created, nil
|
|
}
|
|
|
|
// DeletePolicy removes a policy rule by ID.
|
|
func (c *VaultClient) DeletePolicy(ctx context.Context, token, id string) error {
|
|
_, err := c.policy.DeletePolicy(withToken(ctx, token), &pb.DeletePolicyRequest{Id: id})
|
|
return err
|
|
}
|
|
|
|
func pbToRule(r *pb.PolicyRule) PolicyRule {
|
|
if r == nil {
|
|
return PolicyRule{}
|
|
}
|
|
return PolicyRule{
|
|
ID: r.Id,
|
|
Priority: int(r.Priority),
|
|
Effect: r.Effect,
|
|
Usernames: r.Usernames,
|
|
Roles: r.Roles,
|
|
Resources: r.Resources,
|
|
Actions: r.Actions,
|
|
}
|
|
}
|
|
|
|
func ruleToPB(r PolicyRule) *pb.PolicyRule {
|
|
return &pb.PolicyRule{
|
|
Id: r.ID,
|
|
Priority: int32(r.Priority),
|
|
Effect: r.Effect,
|
|
Usernames: r.Usernames,
|
|
Roles: r.Roles,
|
|
Resources: r.Resources,
|
|
Actions: r.Actions,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SSH CA
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// SSHCAPublicKey holds the CA public key details for display.
|
|
type SSHCAPublicKey struct {
|
|
PublicKey string // authorized_keys format
|
|
}
|
|
|
|
// GetSSHCAPublicKey returns the SSH CA public key for a mount.
|
|
func (c *VaultClient) GetSSHCAPublicKey(ctx context.Context, mount string) (*SSHCAPublicKey, error) {
|
|
resp, err := c.sshca.GetCAPublicKey(ctx, &pb.SSHGetCAPublicKeyRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &SSHCAPublicKey{PublicKey: resp.PublicKey}, nil
|
|
}
|
|
|
|
// SSHCASignRequest holds parameters for signing an SSH certificate.
|
|
type SSHCASignRequest struct {
|
|
PublicKey string
|
|
Principals []string
|
|
Profile string
|
|
TTL string
|
|
}
|
|
|
|
// SSHCASignResult holds the result of signing an SSH certificate.
|
|
type SSHCASignResult struct {
|
|
Serial string
|
|
CertType string
|
|
Principals []string
|
|
CertData string
|
|
KeyID string
|
|
IssuedBy string
|
|
IssuedAt string
|
|
ExpiresAt string
|
|
}
|
|
|
|
func sshSignResultFromHost(resp *pb.SSHSignHostResponse) *SSHCASignResult {
|
|
r := &SSHCASignResult{
|
|
Serial: resp.Serial,
|
|
CertType: resp.CertType,
|
|
Principals: resp.Principals,
|
|
CertData: resp.CertData,
|
|
KeyID: resp.KeyId,
|
|
IssuedBy: resp.IssuedBy,
|
|
}
|
|
if resp.IssuedAt != nil {
|
|
r.IssuedAt = resp.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if resp.ExpiresAt != nil {
|
|
r.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return r
|
|
}
|
|
|
|
func sshSignResultFromUser(resp *pb.SSHSignUserResponse) *SSHCASignResult {
|
|
r := &SSHCASignResult{
|
|
Serial: resp.Serial,
|
|
CertType: resp.CertType,
|
|
Principals: resp.Principals,
|
|
CertData: resp.CertData,
|
|
KeyID: resp.KeyId,
|
|
IssuedBy: resp.IssuedBy,
|
|
}
|
|
if resp.IssuedAt != nil {
|
|
r.IssuedAt = resp.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if resp.ExpiresAt != nil {
|
|
r.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return r
|
|
}
|
|
|
|
// SSHCASignHost signs an SSH host certificate.
|
|
func (c *VaultClient) SSHCASignHost(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error) {
|
|
hostname := ""
|
|
if len(req.Principals) > 0 {
|
|
hostname = req.Principals[0]
|
|
}
|
|
resp, err := c.sshca.SignHost(withToken(ctx, token), &pb.SSHSignHostRequest{
|
|
Mount: mount,
|
|
PublicKey: req.PublicKey,
|
|
Hostname: hostname,
|
|
Ttl: req.TTL,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sshSignResultFromHost(resp), nil
|
|
}
|
|
|
|
// SSHCASignUser signs an SSH user certificate.
|
|
func (c *VaultClient) SSHCASignUser(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error) {
|
|
resp, err := c.sshca.SignUser(withToken(ctx, token), &pb.SSHSignUserRequest{
|
|
Mount: mount,
|
|
PublicKey: req.PublicKey,
|
|
Principals: req.Principals,
|
|
Profile: req.Profile,
|
|
Ttl: req.TTL,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sshSignResultFromUser(resp), nil
|
|
}
|
|
|
|
// SSHCAProfileSummary holds lightweight profile data for list views.
|
|
type SSHCAProfileSummary struct {
|
|
Name string
|
|
}
|
|
|
|
// ListSSHCAProfiles returns all signing profile names for a mount.
|
|
func (c *VaultClient) ListSSHCAProfiles(ctx context.Context, token, mount string) ([]SSHCAProfileSummary, error) {
|
|
resp, err := c.sshca.ListProfiles(withToken(ctx, token), &pb.SSHListProfilesRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profiles := make([]SSHCAProfileSummary, 0, len(resp.Profiles))
|
|
for _, name := range resp.Profiles {
|
|
profiles = append(profiles, SSHCAProfileSummary{Name: name})
|
|
}
|
|
return profiles, nil
|
|
}
|
|
|
|
// SSHCAProfile holds full profile data for the detail view.
|
|
type SSHCAProfile struct {
|
|
Name string
|
|
CriticalOptions map[string]string
|
|
Extensions map[string]string
|
|
MaxTTL string
|
|
AllowedPrincipals []string
|
|
}
|
|
|
|
// GetSSHCAProfile retrieves a signing profile by name.
|
|
func (c *VaultClient) GetSSHCAProfile(ctx context.Context, token, mount, name string) (*SSHCAProfile, error) {
|
|
resp, err := c.sshca.GetProfile(withToken(ctx, token), &pb.SSHGetProfileRequest{Mount: mount, Name: name})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &SSHCAProfile{
|
|
Name: resp.Name,
|
|
CriticalOptions: resp.CriticalOptions,
|
|
Extensions: resp.Extensions,
|
|
MaxTTL: resp.MaxTtl,
|
|
AllowedPrincipals: resp.AllowedPrincipals,
|
|
}, nil
|
|
}
|
|
|
|
// SSHCAProfileRequest holds parameters for creating or updating a profile.
|
|
type SSHCAProfileRequest struct {
|
|
Name string
|
|
CriticalOptions map[string]string
|
|
Extensions map[string]string
|
|
MaxTTL string
|
|
AllowedPrincipals []string
|
|
}
|
|
|
|
// CreateSSHCAProfile creates a new signing profile.
|
|
func (c *VaultClient) CreateSSHCAProfile(ctx context.Context, token, mount string, req SSHCAProfileRequest) error {
|
|
_, err := c.sshca.CreateProfile(withToken(ctx, token), &pb.SSHCreateProfileRequest{
|
|
Mount: mount,
|
|
Name: req.Name,
|
|
CriticalOptions: req.CriticalOptions,
|
|
Extensions: req.Extensions,
|
|
MaxTtl: req.MaxTTL,
|
|
AllowedPrincipals: req.AllowedPrincipals,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// UpdateSSHCAProfile updates an existing signing profile.
|
|
func (c *VaultClient) UpdateSSHCAProfile(ctx context.Context, token, mount, name string, req SSHCAProfileRequest) error {
|
|
_, err := c.sshca.UpdateProfile(withToken(ctx, token), &pb.SSHUpdateProfileRequest{
|
|
Mount: mount,
|
|
Name: name,
|
|
CriticalOptions: req.CriticalOptions,
|
|
Extensions: req.Extensions,
|
|
MaxTtl: req.MaxTTL,
|
|
AllowedPrincipals: req.AllowedPrincipals,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// DeleteSSHCAProfile removes a signing profile.
|
|
func (c *VaultClient) DeleteSSHCAProfile(ctx context.Context, token, mount, name string) error {
|
|
_, err := c.sshca.DeleteProfile(withToken(ctx, token), &pb.SSHDeleteProfileRequest{Mount: mount, Name: name})
|
|
return err
|
|
}
|
|
|
|
// SSHCACertSummary holds lightweight cert data for list views.
|
|
type SSHCACertSummary struct {
|
|
Serial string
|
|
CertType string
|
|
Principals string
|
|
KeyID string
|
|
Profile string
|
|
IssuedBy string
|
|
IssuedAt string
|
|
ExpiresAt string
|
|
Revoked bool
|
|
}
|
|
|
|
// ListSSHCACerts returns all SSH certificate summaries for a mount.
|
|
func (c *VaultClient) ListSSHCACerts(ctx context.Context, token, mount string) ([]SSHCACertSummary, error) {
|
|
resp, err := c.sshca.ListCerts(withToken(ctx, token), &pb.SSHListCertsRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs := make([]SSHCACertSummary, 0, len(resp.Certs))
|
|
for _, s := range resp.Certs {
|
|
cs := SSHCACertSummary{
|
|
Serial: s.Serial,
|
|
CertType: s.CertType,
|
|
Principals: strings.Join(s.Principals, ", "),
|
|
KeyID: s.KeyId,
|
|
Profile: s.Profile,
|
|
IssuedBy: s.IssuedBy,
|
|
Revoked: s.Revoked,
|
|
}
|
|
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
|
|
}
|
|
|
|
// SSHCACertDetail holds full SSH certificate data for the detail view.
|
|
type SSHCACertDetail struct {
|
|
Serial string
|
|
CertType string
|
|
Principals []string
|
|
CertData string
|
|
KeyID string
|
|
Profile string
|
|
IssuedBy string
|
|
IssuedAt string
|
|
ExpiresAt string
|
|
Revoked bool
|
|
RevokedAt string
|
|
RevokedBy string
|
|
}
|
|
|
|
// GetSSHCACert retrieves a full SSH certificate record by serial.
|
|
func (c *VaultClient) GetSSHCACert(ctx context.Context, token, mount, serial string) (*SSHCACertDetail, error) {
|
|
resp, err := c.sshca.GetCert(withToken(ctx, token), &pb.SSHGetCertRequest{Mount: mount, Serial: serial})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rec := resp.GetCert()
|
|
if rec == nil {
|
|
return nil, fmt.Errorf("cert not found")
|
|
}
|
|
cd := &SSHCACertDetail{
|
|
Serial: rec.Serial,
|
|
CertType: rec.CertType,
|
|
Principals: rec.Principals,
|
|
CertData: rec.CertData,
|
|
KeyID: rec.KeyId,
|
|
Profile: rec.Profile,
|
|
IssuedBy: rec.IssuedBy,
|
|
Revoked: rec.Revoked,
|
|
RevokedBy: rec.RevokedBy,
|
|
}
|
|
if rec.IssuedAt != nil {
|
|
cd.IssuedAt = rec.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if rec.ExpiresAt != nil {
|
|
cd.ExpiresAt = rec.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
if rec.RevokedAt != nil {
|
|
cd.RevokedAt = rec.RevokedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
|
}
|
|
return cd, nil
|
|
}
|
|
|
|
// RevokeSSHCACert marks an SSH certificate as revoked.
|
|
func (c *VaultClient) RevokeSSHCACert(ctx context.Context, token, mount, serial string) error {
|
|
_, err := c.sshca.RevokeCert(withToken(ctx, token), &pb.SSHRevokeCertRequest{Mount: mount, Serial: serial})
|
|
return err
|
|
}
|
|
|
|
// DeleteSSHCACert permanently removes an SSH certificate record.
|
|
func (c *VaultClient) DeleteSSHCACert(ctx context.Context, token, mount, serial string) error {
|
|
_, err := c.sshca.DeleteCert(withToken(ctx, token), &pb.SSHDeleteCertRequest{Mount: mount, Serial: serial})
|
|
return err
|
|
}
|
|
|
|
// GetSSHCAKRL returns the binary KRL data for a mount.
|
|
func (c *VaultClient) GetSSHCAKRL(ctx context.Context, mount string) ([]byte, error) {
|
|
resp, err := c.sshca.GetKRL(ctx, &pb.SSHGetKRLRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Krl, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Transit
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TransitKeySummary holds lightweight key data for list views.
|
|
type TransitKeySummary struct {
|
|
Name string
|
|
}
|
|
|
|
// ListTransitKeys returns all key names for a mount.
|
|
func (c *VaultClient) ListTransitKeys(ctx context.Context, token, mount string) ([]TransitKeySummary, error) {
|
|
resp, err := c.transit.ListKeys(withToken(ctx, token), &pb.ListTransitKeysRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys := make([]TransitKeySummary, 0, len(resp.Keys))
|
|
for _, name := range resp.Keys {
|
|
keys = append(keys, TransitKeySummary{Name: name})
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// TransitKeyDetail holds full key metadata for the detail view.
|
|
type TransitKeyDetail struct {
|
|
Name string
|
|
Type string
|
|
CurrentVersion int
|
|
MinDecryptionVersion int
|
|
AllowDeletion bool
|
|
Versions []int
|
|
}
|
|
|
|
// GetTransitKey retrieves key metadata.
|
|
func (c *VaultClient) GetTransitKey(ctx context.Context, token, mount, name string) (*TransitKeyDetail, error) {
|
|
resp, err := c.transit.GetKey(withToken(ctx, token), &pb.GetTransitKeyRequest{Mount: mount, Name: name})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
versions := make([]int, 0, len(resp.Versions))
|
|
for _, v := range resp.Versions {
|
|
versions = append(versions, int(v))
|
|
}
|
|
return &TransitKeyDetail{
|
|
Name: resp.Name,
|
|
Type: resp.Type,
|
|
CurrentVersion: int(resp.CurrentVersion),
|
|
MinDecryptionVersion: int(resp.MinDecryptionVersion),
|
|
AllowDeletion: resp.AllowDeletion,
|
|
Versions: versions,
|
|
}, nil
|
|
}
|
|
|
|
// CreateTransitKey creates a new named key.
|
|
func (c *VaultClient) CreateTransitKey(ctx context.Context, token, mount, name, keyType string) error {
|
|
_, err := c.transit.CreateKey(withToken(ctx, token), &pb.CreateTransitKeyRequest{
|
|
Mount: mount,
|
|
Name: name,
|
|
Type: keyType,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// DeleteTransitKey permanently removes a named key.
|
|
func (c *VaultClient) DeleteTransitKey(ctx context.Context, token, mount, name string) error {
|
|
_, err := c.transit.DeleteKey(withToken(ctx, token), &pb.DeleteTransitKeyRequest{Mount: mount, Name: name})
|
|
return err
|
|
}
|
|
|
|
// RotateTransitKey creates a new version of the named key.
|
|
func (c *VaultClient) RotateTransitKey(ctx context.Context, token, mount, name string) error {
|
|
_, err := c.transit.RotateKey(withToken(ctx, token), &pb.RotateTransitKeyRequest{Mount: mount, Name: name})
|
|
return err
|
|
}
|
|
|
|
// UpdateTransitKeyConfig updates key config (min_decryption_version, allow_deletion).
|
|
func (c *VaultClient) UpdateTransitKeyConfig(ctx context.Context, token, mount, name string, minDecryptVersion int, allowDeletion bool) error {
|
|
_, err := c.transit.UpdateKeyConfig(withToken(ctx, token), &pb.UpdateTransitKeyConfigRequest{
|
|
Mount: mount,
|
|
Name: name,
|
|
MinDecryptionVersion: int32(minDecryptVersion),
|
|
AllowDeletion: allowDeletion,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// TrimTransitKey deletes old key versions below min_decryption_version.
|
|
func (c *VaultClient) TrimTransitKey(ctx context.Context, token, mount, name string) (int, error) {
|
|
resp, err := c.transit.TrimKey(withToken(ctx, token), &pb.TrimTransitKeyRequest{Mount: mount, Name: name})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int(resp.Trimmed), nil
|
|
}
|
|
|
|
// TransitEncrypt encrypts plaintext with a named key.
|
|
func (c *VaultClient) TransitEncrypt(ctx context.Context, token, mount, key, plaintext, transitCtx string) (string, error) {
|
|
resp, err := c.transit.Encrypt(withToken(ctx, token), &pb.TransitEncryptRequest{
|
|
Mount: mount,
|
|
Key: key,
|
|
Plaintext: plaintext,
|
|
Context: transitCtx,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Ciphertext, nil
|
|
}
|
|
|
|
// TransitDecrypt decrypts ciphertext with a named key.
|
|
func (c *VaultClient) TransitDecrypt(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error) {
|
|
resp, err := c.transit.Decrypt(withToken(ctx, token), &pb.TransitDecryptRequest{
|
|
Mount: mount,
|
|
Key: key,
|
|
Ciphertext: ciphertext,
|
|
Context: transitCtx,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Plaintext, nil
|
|
}
|
|
|
|
// TransitRewrap re-encrypts ciphertext with the latest key version.
|
|
func (c *VaultClient) TransitRewrap(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error) {
|
|
resp, err := c.transit.Rewrap(withToken(ctx, token), &pb.TransitRewrapRequest{
|
|
Mount: mount,
|
|
Key: key,
|
|
Ciphertext: ciphertext,
|
|
Context: transitCtx,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Ciphertext, nil
|
|
}
|
|
|
|
// TransitSign signs input data with an asymmetric key.
|
|
func (c *VaultClient) TransitSign(ctx context.Context, token, mount, key, input string) (string, error) {
|
|
resp, err := c.transit.Sign(withToken(ctx, token), &pb.TransitSignRequest{
|
|
Mount: mount,
|
|
Key: key,
|
|
Input: input,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Signature, nil
|
|
}
|
|
|
|
// TransitVerify verifies a signature against input data.
|
|
func (c *VaultClient) TransitVerify(ctx context.Context, token, mount, key, input, signature string) (bool, error) {
|
|
resp, err := c.transit.Verify(withToken(ctx, token), &pb.TransitVerifyRequest{
|
|
Mount: mount,
|
|
Key: key,
|
|
Input: input,
|
|
Signature: signature,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return resp.Valid, nil
|
|
}
|
|
|
|
// TransitHMAC computes an HMAC.
|
|
func (c *VaultClient) TransitHMAC(ctx context.Context, token, mount, key, input string) (string, error) {
|
|
resp, err := c.transit.Hmac(withToken(ctx, token), &pb.TransitHmacRequest{
|
|
Mount: mount,
|
|
Key: key,
|
|
Input: input,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Hmac, nil
|
|
}
|
|
|
|
// GetTransitPublicKey returns the public key for an asymmetric transit key.
|
|
func (c *VaultClient) GetTransitPublicKey(ctx context.Context, token, mount, name string) (string, error) {
|
|
resp, err := c.transit.GetPublicKey(withToken(ctx, token), &pb.GetTransitPublicKeyRequest{Mount: mount, Name: name})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.PublicKey, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// User (E2E Encryption)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// UserKeyInfo holds a user's public key details.
|
|
type UserKeyInfo struct {
|
|
Username string
|
|
PublicKey string
|
|
Algorithm string
|
|
}
|
|
|
|
// UserRegister self-registers the caller.
|
|
func (c *VaultClient) UserRegister(ctx context.Context, token, mount string) (*UserKeyInfo, error) {
|
|
resp, err := c.user.Register(withToken(ctx, token), &pb.UserRegisterRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &UserKeyInfo{
|
|
Username: resp.Username,
|
|
PublicKey: resp.PublicKey,
|
|
Algorithm: resp.Algorithm,
|
|
}, nil
|
|
}
|
|
|
|
// UserProvision creates a keypair for a given username. Admin only.
|
|
func (c *VaultClient) UserProvision(ctx context.Context, token, mount, username string) (*UserKeyInfo, error) {
|
|
resp, err := c.user.Provision(withToken(ctx, token), &pb.UserProvisionRequest{Mount: mount, Username: username})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &UserKeyInfo{
|
|
Username: resp.Username,
|
|
PublicKey: resp.PublicKey,
|
|
Algorithm: resp.Algorithm,
|
|
}, nil
|
|
}
|
|
|
|
// GetUserPublicKey returns the public key for a username.
|
|
func (c *VaultClient) GetUserPublicKey(ctx context.Context, token, mount, username string) (*UserKeyInfo, error) {
|
|
resp, err := c.user.GetPublicKey(withToken(ctx, token), &pb.UserGetPublicKeyRequest{Mount: mount, Username: username})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &UserKeyInfo{
|
|
Username: resp.Username,
|
|
PublicKey: resp.PublicKey,
|
|
Algorithm: resp.Algorithm,
|
|
}, nil
|
|
}
|
|
|
|
// ListUsers returns all registered usernames.
|
|
func (c *VaultClient) ListUsers(ctx context.Context, token, mount string) ([]string, error) {
|
|
resp, err := c.user.ListUsers(withToken(ctx, token), &pb.UserListUsersRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Users, nil
|
|
}
|
|
|
|
// UserEncrypt encrypts plaintext for one or more recipients.
|
|
func (c *VaultClient) UserEncrypt(ctx context.Context, token, mount, plaintext, userMetadata string, recipients []string) (string, error) {
|
|
resp, err := c.user.Encrypt(withToken(ctx, token), &pb.UserEncryptRequest{
|
|
Mount: mount,
|
|
Plaintext: plaintext,
|
|
Metadata: userMetadata,
|
|
Recipients: recipients,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Envelope, nil
|
|
}
|
|
|
|
// UserDecryptResult holds the result of decrypting an envelope.
|
|
type UserDecryptResult struct {
|
|
Plaintext string
|
|
Sender string
|
|
Metadata string
|
|
}
|
|
|
|
// UserDecrypt decrypts an envelope addressed to the caller.
|
|
func (c *VaultClient) UserDecrypt(ctx context.Context, token, mount, envelope string) (*UserDecryptResult, error) {
|
|
resp, err := c.user.Decrypt(withToken(ctx, token), &pb.UserDecryptRequest{Mount: mount, Envelope: envelope})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &UserDecryptResult{
|
|
Plaintext: resp.Plaintext,
|
|
Sender: resp.Sender,
|
|
Metadata: resp.Metadata,
|
|
}, nil
|
|
}
|
|
|
|
// UserReEncrypt re-encrypts an envelope with current keys.
|
|
func (c *VaultClient) UserReEncrypt(ctx context.Context, token, mount, envelope string) (string, error) {
|
|
resp, err := c.user.ReEncrypt(withToken(ctx, token), &pb.UserReEncryptRequest{Mount: mount, Envelope: envelope})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Envelope, nil
|
|
}
|
|
|
|
// UserRotateKey generates a new keypair for the caller.
|
|
func (c *VaultClient) UserRotateKey(ctx context.Context, token, mount string) (*UserKeyInfo, error) {
|
|
resp, err := c.user.RotateKey(withToken(ctx, token), &pb.UserRotateKeyRequest{Mount: mount})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &UserKeyInfo{
|
|
Username: resp.Username,
|
|
PublicKey: resp.PublicKey,
|
|
Algorithm: resp.Algorithm,
|
|
}, nil
|
|
}
|
|
|
|
// UserDeleteUser removes a user's keys. Admin only.
|
|
func (c *VaultClient) UserDeleteUser(ctx context.Context, token, mount, username string) error {
|
|
_, err := c.user.DeleteUser(withToken(ctx, token), &pb.UserDeleteUserRequest{Mount: mount, Username: username})
|
|
return err
|
|
}
|