VAULT-41681: SSH certificate observations (#11811) (#11834)

* ssh observations and tests

* remove unnecessary comments

* add metadata in comments

* add more assertions, fix test

* fix test

Co-authored-by: miagilepner <mia.epner@hashicorp.com>
This commit is contained in:
Vault Automation 2026-01-19 09:22:04 -07:00 committed by GitHub
parent 539e30c4cd
commit 87c9b9470b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 202 additions and 38 deletions

View file

@ -135,6 +135,14 @@ SjOQL/GkH1nkRcDS9++aAAAAAmNhAQID
dockerImageTagSupportsNoRSA1 = "8.4_p1-r3-ls48"
)
var caObservationFields = []string{
"ttl", "max_ttl", "allow_user_certificates", "allow_host_certificates",
"allow_bare_domains", "allow_subdomains", "allow_user_key_ids",
"allowed_users_template", "allowed_domains_template", "default_user_template",
"default_extensions_template", "algorithm_signer", "not_before_duration",
"allow_empty_principals",
}
var ctx = context.Background()
func prepareTestContainer(t *testing.T, tag, caPublicKeyPEM string) (func(), string) {
@ -773,10 +781,11 @@ func TestSSHBackend_VerifyEcho(t *testing.T) {
expectedData := map[string]interface{}{
"message": api.VerifyEchoResponse,
}
obsRecorder := observations.NewTestObservationRecorder()
logicaltest.Test(t, logicaltest.TestCase{
LogicalFactory: newTestingFactory(t, nil),
LogicalFactory: newTestingFactory(t, obsRecorder),
Steps: []logicaltest.TestStep{
testVerifyWrite(t, verifyData, expectedData),
testVerifyWrite(t, verifyData, expectedData, obsRecorder),
},
})
}
@ -1007,7 +1016,7 @@ cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA==
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(caPublicKey, caPrivateKey),
configCaStep(caPublicKey, caPrivateKey, obsRecorder),
testRoleWrite(t, "testcarole", roleOptions, obsRecorder),
{
Operation: logical.UpdateOperation,
@ -1050,19 +1059,27 @@ cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA==
if !expectError && err != nil {
return err
}
obs := obsRecorder.LastObservationOfType(ObservationTypeSSHSign)
if obs == nil {
return errors.New("no SSH sign observation recorded")
}
if obs.Data["role_name"] != "testcarole" {
return fmt.Errorf("expected role_name %q, got %q", "testcarole", obs.Data["role_name"])
}
return nil
},
},
testIssueCert("testcarole", "ec", testUserName, sshAddress, expectError),
testIssueCert("testcarole", "ed25519", testUserName, sshAddress, expectError),
testIssueCert("testcarole", "rsa", testUserName, sshAddress, expectError),
testIssueCert("testcarole", "ec", testUserName, sshAddress, expectError, obsRecorder),
testIssueCert("testcarole", "ed25519", testUserName, sshAddress, expectError, obsRecorder),
testIssueCert("testcarole", "rsa", testUserName, sshAddress, expectError, obsRecorder),
},
}
logicaltest.Test(t, testCase)
}
func testIssueCert(role string, keyType string, testUserName string, sshAddress string, expectError bool) logicaltest.TestStep {
func testIssueCert(role string, keyType string, testUserName string, sshAddress string, expectError bool, obsRecorder *observations.TestObservationRecorder) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "issue/" + role,
@ -1105,6 +1122,32 @@ func testIssueCert(role string, keyType string, testUserName string, sshAddress
return err
}
if obsRecorder == nil {
return nil
}
obs := obsRecorder.LastObservationOfType(ObservationTypeSSHIssue)
if obs == nil {
return errors.New("no SSH issue observation recorded")
}
if obs.Data["role_name"] != role {
return fmt.Errorf("expected role_name %q, got %q", role, obs.Data["role_name"])
}
if obs.Data["key_type"] == nil {
return fmt.Errorf("missing key_type in observation metadata")
}
if obs.Data["certificate_type"] == nil {
return fmt.Errorf("missing certificate_type in observation metadata")
}
if obs.Data["serial_number"] == nil {
return fmt.Errorf("missing serial_number in observation metadata")
}
if obs.Data["key_id"] == nil {
return fmt.Errorf("missing key_id in observation metadata")
}
if _, exists := obs.Data["ttl"]; !exists {
return fmt.Errorf("missing ttl in observation metadata")
}
return nil
},
}
@ -1186,7 +1229,7 @@ cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA==
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, obsRecorder),
testRoleWrite(t, "testcarole", roleOptionsOldEntry, obsRecorder),
testRoleWrite(t, "testcarole", roleOptionsUpgradedEntry, obsRecorder),
{
@ -1243,7 +1286,7 @@ func TestBackend_AbleToRetrievePublicKey(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
{
Operation: logical.ReadOperation,
@ -1325,7 +1368,7 @@ func TestBackend_ValidPrincipalsValidatedForHostCertificates(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("testing", map[string]interface{}{
"key_type": "ca",
@ -1368,7 +1411,7 @@ func TestBackend_OptionsOverrideDefaults(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("testing", map[string]interface{}{
"key_type": "ca",
@ -1415,7 +1458,7 @@ func TestBackend_EmptyPrincipals(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("no_user_principals", map[string]interface{}{
"key_type": "ca",
"allow_user_certificates": true,
@ -1489,7 +1532,7 @@ func TestBackend_AllowedUserKeyLengths(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("weakkey", map[string]interface{}{
"key_type": "ca",
"allow_user_certificates": true,
@ -1662,7 +1705,7 @@ func TestBackend_CustomKeyIDFormat(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("customrole", map[string]interface{}{
"key_type": "ca",
@ -1711,7 +1754,7 @@ func TestBackend_DisallowUserProvidedKeyIDs(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("testing", map[string]interface{}{
"key_type": "ca",
@ -1976,7 +2019,7 @@ func TestSSHBackend_ValidateNotBeforeDuration(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("testing", map[string]interface{}{
"key_type": "ca",
@ -2071,7 +2114,7 @@ func TestSSHBackend_IssueSign(t *testing.T) {
testCase := logicaltest.TestCase{
LogicalBackend: b,
Steps: []logicaltest.TestStep{
configCaStep(testCAPublicKey, testCAPrivateKey),
configCaStep(testCAPublicKey, testCAPrivateKey, nil),
createRoleStep("testing", map[string]interface{}{
"key_type": "otp",
@ -2304,7 +2347,7 @@ func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
)
}
func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
func configCaStep(caPublicKey, caPrivateKey string, obsRecorder *observations.TestObservationRecorder) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config/ca",
@ -2312,6 +2355,17 @@ func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
"public_key": caPublicKey,
"private_key": caPrivateKey,
},
Check: func(r *logical.Response) error {
if obsRecorder == nil {
return nil
}
obs := obsRecorder.LastObservationOfType(ObservationTypeSSHConfigCAWrite)
if obs == nil {
return errors.New("no SSH config CA write observation recorded")
}
return nil
},
}
}
@ -2540,7 +2594,7 @@ func testConfigZeroAddressRead(t *testing.T, expected map[string]interface{}, ob
}
}
func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[string]interface{}) logicaltest.TestStep {
func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[string]interface{}, obsRecorder *observations.TestObservationRecorder) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: fmt.Sprintf("verify"),
@ -2558,6 +2612,17 @@ func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[str
if !reflect.DeepEqual(ac, ex) {
return fmt.Errorf("invalid response")
}
if obsRecorder != nil && data["otp"] != api.VerifyEchoRequest {
lastObservation := obsRecorder.LastObservationOfType(ObservationTypeSSHOTPVerify)
if lastObservation == nil {
return fmt.Errorf("missing OTP verify observation")
}
if lastObservation.Data["role_name"] == nil {
return fmt.Errorf("missing role_name in OTP verify observation metadata")
}
}
return nil
},
}
@ -2610,6 +2675,15 @@ func testRoleWrite(t *testing.T, name string, data map[string]interface{}, obsRe
if lastObservation.Data["key_type"] != data["key_type"] {
return fmt.Errorf("invalid observation data: \nactual:%#v\nexpected:%#v", lastObservation.Data["key_type"], data["key_type"])
}
if data["key_type"] == KeyTypeCA {
for _, field := range caObservationFields {
if _, exists := lastObservation.Data[field]; !exists {
return fmt.Errorf("missing CA-specific field %q in observation metadata for CA role", field)
}
}
}
return nil
},
}
@ -2670,6 +2744,15 @@ func testRoleRead(t *testing.T, roleName string, expected map[string]interface{}
if lastObservation.Data["key_type"] != d.KeyType {
return fmt.Errorf("invalid observation data: \nactual:%#v\nexpected:%#v", lastObservation.Data["key_type"], d.KeyType)
}
if d.KeyType == KeyTypeCA {
for _, field := range caObservationFields {
if _, exists := lastObservation.Data[field]; !exists {
return fmt.Errorf("missing CA-specific field %q in observation metadata for CA role", field)
}
}
}
return nil
},
}
@ -2754,12 +2837,14 @@ func testCredsWrite(t *testing.T, roleName string, data map[string]interface{},
if lastObservation == nil {
return fmt.Errorf("missing observation")
}
if lastObservation.Data["role_name"] != roleName {
return fmt.Errorf("invalid observation data: \nactual:%#v\nexpected:%#v", lastObservation.Data["role_name"], roleName)
}
if lastObservation.Data["key_type"] != KeyTypeOTP {
return fmt.Errorf("invalid observation data: \nactual:%#v\nexpected:%#v", lastObservation.Data["key_type"], KeyTypeOTP)
}
return nil
},
}
@ -2768,6 +2853,8 @@ func testCredsWrite(t *testing.T, roleName string, data map[string]interface{},
func TestBackend_CleanupDynamicHostKeys(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
obsRecorder := observations.NewTestObservationRecorder()
config.ObservationRecorder = obsRecorder
b, err := Backend(config)
if err != nil {
t.Fatal(err)
@ -2790,6 +2877,9 @@ func TestBackend_CleanupDynamicHostKeys(t *testing.T) {
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["message"])
require.Contains(t, resp.Data["message"], "0 of 0")
obs := obsRecorder.LastObservationOfType(ObservationTypeSSHTidyDynamicKeys)
require.NotNil(t, obs)
require.Equal(t, 0, obs.Data["keys_deleted"])
// Write a bunch of bogus entries.
for i := 0; i < 15; i++ {
data := map[string]interface{}{
@ -2809,6 +2899,9 @@ func TestBackend_CleanupDynamicHostKeys(t *testing.T) {
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["message"])
require.Contains(t, resp.Data["message"], "15 of 15")
obs = obsRecorder.LastObservationOfType(ObservationTypeSSHTidyDynamicKeys)
require.NotNil(t, obs)
require.Equal(t, 15, obs.Data["keys_deleted"])
// Should have none left.
resp, err = b.HandleRequest(context.Background(), cleanRequest)
@ -2817,6 +2910,9 @@ func TestBackend_CleanupDynamicHostKeys(t *testing.T) {
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["message"])
require.Contains(t, resp.Data["message"], "0 of 0")
obs = obsRecorder.LastObservationOfType(ObservationTypeSSHTidyDynamicKeys)
require.NotNil(t, obs)
require.Equal(t, 0, obs.Data["keys_deleted"])
}
type pathAuthCheckerFunc func(t *testing.T, client *api.Client, path string, token string)

View file

@ -36,4 +36,28 @@ const (
// ObservationTypeSSHLookup - Metadata: role_names ([]string)
ObservationTypeSSHLookup = "ssh/lookup"
// ObservationTypeSSHConfigCARead - Metadata: none
ObservationTypeSSHConfigCARead = "ssh/config/ca/read"
// ObservationTypeSSHConfigCAWrite - Metadata: conditionally:
// managed_key_name, managed_key_id (if using managed key), or key_type, key_bits (if generating)
ObservationTypeSSHConfigCAWrite = "ssh/config/ca/write"
// ObservationTypeSSHConfigCADelete - Metadata: none
ObservationTypeSSHConfigCADelete = "ssh/config/ca/delete"
// ObservationTypeSSHSign - Metadata: role_name, key_type, certificate_type, ttl, serial_number,
// key_id, and for CA roles: max_ttl, allow_user_certificates, allow_host_certificates,
// allow_bare_domains, allow_subdomains, allow_user_key_ids, allowed_users_template,
// allowed_domains_template, default_user_template, default_extensions_template,
// algorithm_signer, not_before_duration, allow_empty_principals
ObservationTypeSSHSign = "ssh/certificate/sign"
// ObservationTypeSSHIssue - Metadata: role_name, key_type (from keySpecs), key_bits,
// certificate_type, ttl, serial_number, key_id, and for CA roles: max_ttl,
// allow_user_certificates, allow_host_certificates, allow_bare_domains, allow_subdomains,
// allow_user_key_ids, allowed_users_template, allowed_domains_template, default_user_template,
// default_extensions_template, algorithm_signer, not_before_duration, allow_empty_principals
ObservationTypeSSHIssue = "ssh/certificate/issue"
// ObservationTypeSSHTidyDynamicKeys - Metadata: keys_deleted (int)
ObservationTypeSSHTidyDynamicKeys = "ssh/tidy/dynamic-keys"
)

View file

@ -42,6 +42,10 @@ func (b *backend) handleCleanupKeys(ctx context.Context, req *logical.Request, d
}
}
b.Backend.TryRecordObservationWithRequest(ctx, req, ObservationTypeSSHTidyDynamicKeys, map[string]interface{}{
"keys_deleted": len(names),
})
return &logical.Response{
Data: map[string]interface{}{
"message": fmt.Sprintf("Removed %v of %v host keys.", len(names), len(names)),

View file

@ -134,6 +134,8 @@ func (b *backend) pathConfigCARead(ctx context.Context, req *logical.Request, da
return logical.ErrorResponse("keys haven't been configured yet"), nil
}
b.Backend.TryRecordObservationWithRequest(ctx, req, ObservationTypeSSHConfigCARead, nil)
response := &logical.Response{
Data: map[string]interface{}{
"public_key": publicKey,
@ -159,6 +161,9 @@ func (b *backend) pathConfigCADelete(ctx context.Context, req *logical.Request,
if err := req.Storage.Delete(ctx, caManagedKeyStoragePath); err != nil {
return nil, err
}
b.Backend.TryRecordObservationWithRequest(ctx, req, ObservationTypeSSHConfigCADelete, nil)
return nil, nil
}
@ -247,12 +252,16 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request,
generateSigningKey := data.Get("generate_signing_key").(bool)
metadata := make(map[string]interface{})
if useManagedKey {
generateSigningKey = false
err = b.createManagedKey(ctx, req.Storage, managedKeyName, managedKeyID)
if err != nil {
return nil, err
}
metadata["managed_key_name"] = managedKeyName
metadata["managed_key_id"] = managedKeyID
} else {
if publicKey != "" && privateKey != "" {
_, err := ssh.ParsePrivateKey([]byte(privateKey))
@ -272,6 +281,8 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request,
if err != nil {
return nil, err
}
metadata["key_type"] = keyType
metadata["key_bits"] = keyBits
} else {
return logical.ErrorResponse("if generate_signing_key is false, either both public_key and private_key or a managed key must be provided"), nil
}
@ -282,6 +293,8 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request,
}
}
b.Backend.TryRecordObservationWithRequest(ctx, req, ObservationTypeSSHConfigCAWrite, metadata)
if generateSigningKey {
response := &logical.Response{
Data: map[string]interface{}{

View file

@ -8,6 +8,7 @@ import (
"crypto/rand"
"errors"
"fmt"
"maps"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
@ -105,10 +106,10 @@ func (b *backend) pathIssue(ctx context.Context, req *logical.Request, data *fra
}
// Issue certificate
return b.pathIssueCertificate(ctx, req, data, role, keySpecs)
return b.pathIssueCertificate(ctx, req, data, role, keySpecs, roleName)
}
func (b *backend) pathIssueCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, keySpecs *keySpecs) (*logical.Response, error) {
func (b *backend) pathIssueCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, keySpecs *keySpecs, roleName string) (*logical.Response, error) {
publicKey, privateKey, err := generateSSHKeyPair(rand.Reader, keySpecs.Type, keySpecs.Bits)
if err != nil {
return nil, err
@ -120,7 +121,7 @@ func (b *backend) pathIssueCertificate(ctx context.Context, req *logical.Request
return logical.ErrorResponse(fmt.Sprintf("failed to parse public_key as SSH key: %s", err)), nil
}
response, err := b.pathSignIssueCertificateHelper(ctx, req, data, role, userPublicKey)
response, certMetadata, err := b.pathSignIssueCertificateHelper(ctx, req, data, role, userPublicKey)
if err != nil {
return nil, err
}
@ -132,6 +133,11 @@ func (b *backend) pathIssueCertificate(ctx context.Context, req *logical.Request
response.Data["private_key"] = privateKey
response.Data["private_key_type"] = keySpecs.Type
metadata := role.observationMetadata(roleName)
metadata["key_type"] = keySpecs.Type
metadata["key_bits"] = keySpecs.Bits
maps.Copy(metadata, certMetadata)
b.TryRecordObservationWithRequest(ctx, req, ObservationTypeSSHIssue, metadata)
return response, nil
}

View file

@ -54,57 +54,57 @@ type creationBundle struct {
Extensions map[string]string
}
func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, publicKey ssh.PublicKey) (*logical.Response, error) {
func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, publicKey ssh.PublicKey) (*logical.Response, map[string]interface{}, error) {
// Note that these various functions always return "user errors" so we pass
// them as 4xx values
keyID, err := b.calculateKeyID(data, req, role, publicKey)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
certificateType, err := b.calculateCertificateType(data, role)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
var parsedPrincipals []string
if certificateType == ssh.HostCert {
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, "", role.AllowedDomains, role.AllowedDomainsTemplate, validateValidPrincipalForHosts(role))
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
} else {
defaultPrincipal := role.DefaultUser
if role.DefaultUserTemplate {
defaultPrincipal, err = b.renderPrincipal(role.DefaultUser, req)
if err != nil {
return nil, err
return nil, nil, err
}
}
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, defaultPrincipal, role.AllowedUsers, role.AllowedUsersTemplate, strutil.StrListContains)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
}
ttl, err := b.calculateTTL(data, role)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
criticalOptions, err := b.calculateCriticalOptions(data, role)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
extensions, addExtTemplatingWarning, err := b.calculateExtensions(data, req, role)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
return logical.ErrorResponse(err.Error()), nil, nil
}
signer, err := b.getCASigner(ctx, req.Storage)
if err != nil {
return nil, fmt.Errorf("error creating signer: %w", err)
return nil, nil, fmt.Errorf("error creating signer: %w", err)
}
cBundle := creationBundle{
@ -121,12 +121,12 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
certificate, err := cBundle.sign()
if err != nil {
return nil, err
return nil, nil, err
}
signedSSHCertificate := ssh.MarshalAuthorizedKey(certificate)
if len(signedSSHCertificate) == 0 {
return nil, errors.New("error marshaling signed certificate")
return nil, nil, errors.New("error marshaling signed certificate")
}
response := &logical.Response{
@ -140,7 +140,14 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
response.AddWarning("default_extension templating enabled with at least one extension requiring identity templating. However, this request lacked identity entity information, causing one or more extensions to be skipped from the generated certificate.")
}
return response, nil
metadata := map[string]interface{}{
"certificate_type": certificateType,
"ttl": ttl.String(),
"serial_number": strconv.FormatUint(certificate.Serial, 16),
"key_id": keyID,
}
return response, metadata, nil
}
func (b *backend) renderPrincipal(principal string, req *logical.Request) (string, error) {

View file

@ -6,6 +6,7 @@ package ssh
import (
"context"
"fmt"
"maps"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
@ -82,10 +83,10 @@ func (b *backend) pathSign(ctx context.Context, req *logical.Request, data *fram
return logical.ErrorResponse(fmt.Sprintf("Unknown role: %s", roleName)), nil
}
return b.pathSignCertificate(ctx, req, data, role)
return b.pathSignCertificate(ctx, req, data, role, roleName)
}
func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole) (*logical.Response, error) {
func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, roleName string) (*logical.Response, error) {
publicKey := data.Get("public_key").(string)
if publicKey == "" {
return logical.ErrorResponse("missing public_key"), nil
@ -101,5 +102,18 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request,
return logical.ErrorResponse(fmt.Sprintf("public_key failed to meet the key requirements: %s", err)), nil
}
return b.pathSignIssueCertificateHelper(ctx, req, data, role, userPublicKey)
response, certMetadata, err := b.pathSignIssueCertificateHelper(ctx, req, data, role, userPublicKey)
if err != nil {
return nil, err
}
if response.IsError() {
return response, nil
}
metadata := role.observationMetadata(roleName)
maps.Copy(metadata, certMetadata)
b.TryRecordObservationWithRequest(ctx, req, ObservationTypeSSHSign, metadata)
return response, nil
}