diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go
index 5dbb94c77..07494dc71 100644
--- a/builtin/logical/pki/backend.go
+++ b/builtin/logical/pki/backend.go
@@ -100,6 +100,7 @@ func Backend(conf *logical.BackendConfig) *backend {
revokedPath,
deltaWALPath,
legacyCRLPath,
+ clusterConfigPath,
"crls/",
"certs/",
},
@@ -127,6 +128,7 @@ func Backend(conf *logical.BackendConfig) *backend {
pathConfigCA(&b),
pathConfigCRL(&b),
pathConfigURLs(&b),
+ pathConfigCluster(&b),
pathSignVerbatim(&b),
pathSign(&b),
pathIssue(&b),
diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go
index 386126a86..c6a79dbfa 100644
--- a/builtin/logical/pki/backend_test.go
+++ b/builtin/logical/pki/backend_test.go
@@ -5966,6 +5966,110 @@ func TestPKI_ListRevokedCerts(t *testing.T) {
require.Equal(t, 4, len(certKeys), "Expected 4 cert entries got %d: %v", len(certKeys), certKeys)
}
+func TestPKI_TemplatedAIAs(t *testing.T) {
+ t.Parallel()
+ b, s := CreateBackendWithStorage(t)
+
+ // Setting templated AIAs should succeed.
+ _, err := CBWrite(b, s, "config/cluster", map[string]interface{}{
+ "path": "http://localhost:8200/v1/pki",
+ })
+ require.NoError(t, err)
+
+ aiaData := map[string]interface{}{
+ "crl_distribution_points": "{{cluster_path}}/issuer/{{issuer_id}}/crl/der",
+ "issuing_certificates": "{{cluster_path}}/issuer/{{issuer_id}}/der",
+ "ocsp_servers": "{{cluster_path}}/ocsp",
+ "enable_templating": true,
+ }
+ _, err = CBWrite(b, s, "config/urls", aiaData)
+ require.NoError(t, err)
+
+ // But root generation will fail.
+ rootData := map[string]interface{}{
+ "common_name": "Long-Lived Root X1",
+ "issuer_name": "long-root-x1",
+ "key_type": "ec",
+ }
+ _, err = CBWrite(b, s, "root/generate/internal", rootData)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to parse AIA URL")
+
+ // Clearing the config and regenerating the root should succeed.
+ _, err = CBWrite(b, s, "config/urls", map[string]interface{}{
+ "crl_distribution_points": "",
+ "issuing_certificates": "",
+ "ocsp_servers": "",
+ "enable_templating": false,
+ })
+ require.NoError(t, err)
+ resp, err := CBWrite(b, s, "root/generate/internal", rootData)
+ requireSuccessNonNilResponse(t, resp, err)
+ issuerId := string(resp.Data["issuer_id"].(issuerID))
+
+ // Now write the original AIA config and sign a leaf.
+ _, err = CBWrite(b, s, "config/urls", aiaData)
+ require.NoError(t, err)
+ _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{
+ "allow_any_name": "true",
+ "key_type": "ec",
+ "ttl": "50m",
+ })
+ require.NoError(t, err)
+ resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{
+ "common_name": "example.com",
+ })
+ requireSuccessNonNilResponse(t, resp, err)
+
+ // Validate the AIA info is correctly templated.
+ cert := parseCert(t, resp.Data["certificate"].(string))
+ require.Equal(t, cert.OCSPServer, []string{"http://localhost:8200/v1/pki/ocsp"})
+ require.Equal(t, cert.IssuingCertificateURL, []string{"http://localhost:8200/v1/pki/issuer/" + issuerId + "/der"})
+ require.Equal(t, cert.CRLDistributionPoints, []string{"http://localhost:8200/v1/pki/issuer/" + issuerId + "/crl/der"})
+
+ // Modify our issuer to set custom AIAs: these URLs are bad.
+ _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{
+ "enable_aia_url_templating": "false",
+ "crl_distribution_points": "a",
+ "issuing_certificates": "b",
+ "ocsp_servers": "c",
+ })
+ require.Error(t, err)
+
+ // These URLs are good.
+ _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{
+ "enable_aia_url_templating": "false",
+ "crl_distribution_points": "http://localhost/a",
+ "issuing_certificates": "http://localhost/b",
+ "ocsp_servers": "http://localhost/c",
+ })
+
+ resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{
+ "common_name": "example.com",
+ })
+ requireSuccessNonNilResponse(t, resp, err)
+
+ // Validate the AIA info is correctly templated.
+ cert = parseCert(t, resp.Data["certificate"].(string))
+ require.Equal(t, cert.OCSPServer, []string{"http://localhost/c"})
+ require.Equal(t, cert.IssuingCertificateURL, []string{"http://localhost/b"})
+ require.Equal(t, cert.CRLDistributionPoints, []string{"http://localhost/a"})
+
+ // These URLs are bad, but will fail at issuance time due to AIA templating.
+ resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{
+ "enable_aia_url_templating": "true",
+ "crl_distribution_points": "a",
+ "issuing_certificates": "b",
+ "ocsp_servers": "c",
+ })
+ requireSuccessNonNilResponse(t, resp, err)
+ require.NotEmpty(t, resp.Warnings)
+ _, err = CBWrite(b, s, "issue/testing", map[string]interface{}{
+ "common_name": "example.com",
+ })
+ require.Error(t, err)
+}
+
var (
initTest sync.Once
rsaCAKey string
diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go
index 560831063..1c763d29c 100644
--- a/builtin/logical/pki/cert_util.go
+++ b/builtin/logical/pki/cert_util.go
@@ -144,7 +144,7 @@ func (sc *storageContext) fetchCAInfoByIssuerId(issuerId issuerID, usage issuerU
entries, err := entry.GetAIAURLs(sc)
if err != nil {
- return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
+ return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch AIA URL information: %v", err)}
}
caInfo.URLs = entries
@@ -699,9 +699,15 @@ func generateCert(sc *storageContext,
// issuer entry yet, we default to the global URLs.
entries, err := getGlobalAIAURLs(ctx, sc.Storage)
if err != nil {
- return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
+ return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch AIA URL information: %v", err)}
}
- data.Params.URLs = entries
+
+ uris, err := entries.toURLEntries(sc, issuerID(""))
+ if err != nil {
+ return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse AIA URL information: %v\nUsing templated AIA URL's {{issuer_id}} field when generating root certificates is not supported.", err)}
+ }
+
+ data.Params.URLs = uris
if input.role.MaxPathLength == nil {
data.Params.MaxPathLength = -1
diff --git a/builtin/logical/pki/path_config_cluster.go b/builtin/logical/pki/path_config_cluster.go
new file mode 100644
index 000000000..440dcc874
--- /dev/null
+++ b/builtin/logical/pki/path_config_cluster.go
@@ -0,0 +1,97 @@
+package pki
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/asaskevich/govalidator"
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+func pathConfigCluster(b *backend) *framework.Path {
+ return &framework.Path{
+ Pattern: "config/cluster",
+ Fields: map[string]*framework.FieldSchema{
+ "path": {
+ Type: framework.TypeString,
+ Description: `Canonical URI to this mount on this performance
+replication cluster's external address. This is for resolving AIA URLs and
+providing the {{cluster_path}} template parameter but might be used for other
+purposes in the future.
+
+This should only point back to this particular PR replica and should not ever
+point to another PR cluster. It may point to any node in the PR replica,
+including standby nodes, and need not always point to the active node.
+
+For example: https://pr1.vault.example.com:8200/v1/pki`,
+ },
+ },
+
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.UpdateOperation: &framework.PathOperation{
+ Callback: b.pathWriteCluster,
+ },
+ logical.ReadOperation: &framework.PathOperation{
+ Callback: b.pathReadCluster,
+ },
+ },
+
+ HelpSynopsis: pathConfigClusterHelpSyn,
+ HelpDescription: pathConfigClusterHelpDesc,
+ }
+}
+
+func (b *backend) pathReadCluster(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
+ sc := b.makeStorageContext(ctx, req.Storage)
+ cfg, err := sc.getClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ resp := &logical.Response{
+ Data: map[string]interface{}{
+ "path": cfg.Path,
+ },
+ }
+
+ return resp, nil
+}
+
+func (b *backend) pathWriteCluster(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
+ sc := b.makeStorageContext(ctx, req.Storage)
+ cfg, err := sc.getClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ cfg.Path = data.Get("path").(string)
+ if !govalidator.IsURL(cfg.Path) {
+ return nil, fmt.Errorf("invalid, non-URL path given to cluster: %v", cfg.Path)
+ }
+
+ if err := sc.writeClusterConfig(cfg); err != nil {
+ return nil, err
+ }
+
+ resp := &logical.Response{
+ Data: map[string]interface{}{
+ "path": cfg.Path,
+ },
+ }
+
+ return resp, nil
+}
+
+const pathConfigClusterHelpSyn = `
+Set cluster-local configuration, including address to this PR cluster.
+`
+
+const pathConfigClusterHelpDesc = `
+This path allows you to set cluster-local configuration, including the
+URI to this performance replication cluster. This allows you to use
+templated AIA URLs with /config/urls and /issuer/:issuer_ref, setting the
+reference to the cluster's URI.
+
+Only one address can be specified for any given cluster.
+`
diff --git a/builtin/logical/pki/path_config_urls.go b/builtin/logical/pki/path_config_urls.go
index 830ca34ad..5b67ad080 100644
--- a/builtin/logical/pki/path_config_urls.go
+++ b/builtin/logical/pki/path_config_urls.go
@@ -3,10 +3,10 @@ package pki
import (
"context"
"fmt"
+ "strings"
"github.com/asaskevich/govalidator"
"github.com/hashicorp/vault/sdk/framework"
- "github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
)
@@ -31,6 +31,16 @@ for the CRL distribution points attribute. See also RFC 5280 Section 4.2.1.13.`,
Description: `Comma-separated list of URLs to be used
for the OCSP servers attribute. See also RFC 5280 Section 4.2.2.1.`,
},
+
+ "enable_templating": {
+ Type: framework.TypeBool,
+ Description: `Whether or not to enabling templating of the
+above AIA fields. When templating is enabled the special values '{{issuer_id}}'
+and '{{cluster_path}}' are available, but the addresses are not checked for
+URI validity until issuance time. This requires /config/cluster's path to be
+set on all PR Secondary clusters.`,
+ Default: false,
+ },
},
Operations: map[logical.Operation]framework.OperationHandler{
@@ -49,7 +59,7 @@ for the OCSP servers attribute. See also RFC 5280 Section 4.2.2.1.`,
func validateURLs(urls []string) string {
for _, curr := range urls {
- if !govalidator.IsURL(curr) {
+ if !govalidator.IsURL(curr) || strings.Contains(curr, "{{issuer_id}}") || strings.Contains(curr, "{{cluster_path}}") {
return curr
}
}
@@ -57,16 +67,17 @@ func validateURLs(urls []string) string {
return ""
}
-func getGlobalAIAURLs(ctx context.Context, storage logical.Storage) (*certutil.URLEntries, error) {
+func getGlobalAIAURLs(ctx context.Context, storage logical.Storage) (*aiaConfigEntry, error) {
entry, err := storage.Get(ctx, "urls")
if err != nil {
return nil, err
}
- entries := &certutil.URLEntries{
+ entries := &aiaConfigEntry{
IssuingCertificates: []string{},
CRLDistributionPoints: []string{},
OCSPServers: []string{},
+ EnableTemplating: false,
}
if entry == nil {
@@ -80,7 +91,7 @@ func getGlobalAIAURLs(ctx context.Context, storage logical.Storage) (*certutil.U
return entries, nil
}
-func writeURLs(ctx context.Context, storage logical.Storage, entries *certutil.URLEntries) error {
+func writeURLs(ctx context.Context, storage logical.Storage, entries *aiaConfigEntry) error {
entry, err := logical.StorageEntryJSON("urls", entries)
if err != nil {
return err
@@ -108,6 +119,7 @@ func (b *backend) pathReadURL(ctx context.Context, req *logical.Request, _ *fram
"issuing_certificates": entries.IssuingCertificates,
"crl_distribution_points": entries.CRLDistributionPoints,
"ocsp_servers": entries.OCSPServers,
+ "enable_templating": entries.EnableTemplating,
},
}
@@ -120,29 +132,68 @@ func (b *backend) pathWriteURL(ctx context.Context, req *logical.Request, data *
return nil, err
}
+ if enableTemplating, ok := data.GetOk("enable_templating"); ok {
+ entries.EnableTemplating = enableTemplating.(bool)
+ }
if urlsInt, ok := data.GetOk("issuing_certificates"); ok {
entries.IssuingCertificates = urlsInt.([]string)
+ }
+ if urlsInt, ok := data.GetOk("crl_distribution_points"); ok {
+ entries.CRLDistributionPoints = urlsInt.([]string)
+ }
+ if urlsInt, ok := data.GetOk("ocsp_servers"); ok {
+ entries.OCSPServers = urlsInt.([]string)
+ }
+
+ resp := &logical.Response{
+ Data: map[string]interface{}{
+ "issuing_certificates": entries.IssuingCertificates,
+ "crl_distribution_points": entries.CRLDistributionPoints,
+ "ocsp_servers": entries.OCSPServers,
+ "enable_templating": entries.EnableTemplating,
+ },
+ }
+
+ if entries.EnableTemplating && !b.useLegacyBundleCaStorage() {
+ sc := b.makeStorageContext(ctx, req.Storage)
+ issuers, err := sc.listIssuers()
+ if err != nil {
+ return nil, fmt.Errorf("unable to read issuers list to validate templated URIs: %w", err)
+ }
+
+ if len(issuers) > 0 {
+ issuer, err := sc.fetchIssuerById(issuers[0])
+ if err != nil {
+ return nil, fmt.Errorf("unable to read issuer to validate templated URIs: %w", err)
+ }
+
+ _, err = entries.toURLEntries(sc, issuer.ID)
+ if err != nil {
+ resp.AddWarning(fmt.Sprintf("issuance may fail: %v\n\nConsider setting the cluster-local address if it is not already set.", err))
+ }
+ }
+ } else if !entries.EnableTemplating {
if badURL := validateURLs(entries.IssuingCertificates); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf(
"invalid URL found in Authority Information Access (AIA) parameter issuing_certificates: %s", badURL)), nil
}
- }
- if urlsInt, ok := data.GetOk("crl_distribution_points"); ok {
- entries.CRLDistributionPoints = urlsInt.([]string)
+
if badURL := validateURLs(entries.CRLDistributionPoints); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf(
"invalid URL found in Authority Information Access (AIA) parameter crl_distribution_points: %s", badURL)), nil
}
- }
- if urlsInt, ok := data.GetOk("ocsp_servers"); ok {
- entries.OCSPServers = urlsInt.([]string)
+
if badURL := validateURLs(entries.OCSPServers); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf(
"invalid URL found in Authority Information Access (AIA) parameter ocsp_servers: %s", badURL)), nil
}
}
- return nil, writeURLs(ctx, req.Storage, entries)
+ if err := writeURLs(ctx, req.Storage, entries); err != nil {
+ return nil, err
+ }
+
+ return resp, nil
}
const pathConfigURLsHelpSyn = `
diff --git a/builtin/logical/pki/path_fetch_issuers.go b/builtin/logical/pki/path_fetch_issuers.go
index ff7645ee7..5cc126213 100644
--- a/builtin/logical/pki/path_fetch_issuers.go
+++ b/builtin/logical/pki/path_fetch_issuers.go
@@ -134,6 +134,15 @@ for the CRL distribution points attribute. See also RFC 5280 Section 4.2.1.13.`,
Description: `Comma-separated list of URLs to be used
for the OCSP servers attribute. See also RFC 5280 Section 4.2.2.1.`,
}
+ fields["enable_aia_url_templating"] = &framework.FieldSchema{
+ Type: framework.TypeBool,
+ Description: `Whether or not to enabling templating of the
+above AIA fields. When templating is enabled the special values '{{issuer_id}}'
+and '{{cluster_path}}' are available, but the addresses are not checked for
+URL validity until issuance time. This requires /config/cluster's path to be
+set on all PR Secondary clusters.`,
+ Default: false,
+ }
return &framework.Path{
// Returns a JSON entry.
@@ -336,16 +345,17 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
}
// AIA access changes
+ enableTemplating := data.Get("enable_aia_url_templating").(bool)
issuerCertificates := data.Get("issuing_certificates").([]string)
- if badURL := validateURLs(issuerCertificates); badURL != "" {
+ if badURL := validateURLs(issuerCertificates); !enableTemplating && badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter issuing_certificates: %s", badURL)), nil
}
crlDistributionPoints := data.Get("crl_distribution_points").([]string)
- if badURL := validateURLs(crlDistributionPoints); badURL != "" {
+ if badURL := validateURLs(crlDistributionPoints); !enableTemplating && badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter crl_distribution_points: %s", badURL)), nil
}
ocspServers := data.Get("ocsp_servers").([]string)
- if badURL := validateURLs(ocspServers); badURL != "" {
+ if badURL := validateURLs(ocspServers); !enableTemplating && badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter ocsp_servers: %s", badURL)), nil
}
@@ -393,7 +403,7 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
}
if issuer.AIAURIs == nil && (len(issuerCertificates) > 0 || len(crlDistributionPoints) > 0 || len(ocspServers) > 0) {
- issuer.AIAURIs = &certutil.URLEntries{}
+ issuer.AIAURIs = &aiaConfigEntry{}
}
if issuer.AIAURIs != nil {
// Associative mapping from data source to destination on the
@@ -424,6 +434,10 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
modified = true
}
}
+ if enableTemplating != issuer.AIAURIs.EnableTemplating {
+ issuer.AIAURIs.EnableTemplating = enableTemplating
+ modified = true
+ }
// If no AIA URLs exist on the issuer, set the AIA URLs entry to nil
// to ease usage later.
@@ -485,6 +499,12 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
if newName != oldName {
addWarningOnDereferencing(sc, oldName, response)
}
+ if issuer.AIAURIs != nil && issuer.AIAURIs.EnableTemplating {
+ _, aiaErr := issuer.AIAURIs.toURLEntries(sc, issuer.ID)
+ if aiaErr != nil {
+ response.AddWarning(fmt.Sprintf("issuance may fail: %v\n\nConsider setting the cluster-local address if it is not already set.", aiaErr))
+ }
+ }
return response, err
}
@@ -629,7 +649,7 @@ func (b *backend) pathPatchIssuer(ctx context.Context, req *logical.Request, dat
// AIA access changes.
if issuer.AIAURIs == nil {
- issuer.AIAURIs = &certutil.URLEntries{}
+ issuer.AIAURIs = &aiaConfigEntry{}
}
// Associative mapping from data source to destination on the
@@ -655,12 +675,20 @@ func (b *backend) pathPatchIssuer(ctx context.Context, req *logical.Request, dat
},
}
+ if enableTemplatingRaw, ok := data.GetOk("enable_aia_url_templating"); ok {
+ enableTemplating := enableTemplatingRaw.(bool)
+ if enableTemplating != issuer.AIAURIs.EnableTemplating {
+ issuer.AIAURIs.EnableTemplating = true
+ modified = true
+ }
+ }
+
// For each pair, if it is different on the object, update it.
for _, pair := range pairs {
rawURLsValue, ok := data.GetOk(pair.Source)
if ok {
urlsValue := rawURLsValue.([]string)
- if badURL := validateURLs(urlsValue); badURL != "" {
+ if badURL := validateURLs(urlsValue); !issuer.AIAURIs.EnableTemplating && badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter %v: %s", pair.Source, badURL)), nil
}
@@ -732,6 +760,12 @@ func (b *backend) pathPatchIssuer(ctx context.Context, req *logical.Request, dat
if newName != oldName {
addWarningOnDereferencing(sc, oldName, response)
}
+ if issuer.AIAURIs != nil && issuer.AIAURIs.EnableTemplating {
+ _, aiaErr := issuer.AIAURIs.toURLEntries(sc, issuer.ID)
+ if aiaErr != nil {
+ response.AddWarning(fmt.Sprintf("issuance may fail: %v\n\nConsider setting the cluster-local address if it is not already set.", aiaErr))
+ }
+ }
return response, err
}
diff --git a/builtin/logical/pki/storage.go b/builtin/logical/pki/storage.go
index e55ea4121..f8861a97c 100644
--- a/builtin/logical/pki/storage.go
+++ b/builtin/logical/pki/storage.go
@@ -30,6 +30,7 @@ const (
deltaCRLPathSuffix = "-delta"
autoTidyConfigPath = "config/auto-tidy"
+ clusterConfigPath = "config/cluster"
// Used as a quick sanity check for a reference id lookups...
uuidLength = 36
@@ -168,7 +169,7 @@ type issuerEntry struct {
Revoked bool `json:"revoked"`
RevocationTime int64 `json:"revocation_time"`
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
- AIAURIs *certutil.URLEntries `json:"aia_uris,omitempty"`
+ AIAURIs *aiaConfigEntry `json:"aia_uris,omitempty"`
LastModified time.Time `json:"last_modified"`
Version uint `json:"version"`
}
@@ -195,6 +196,66 @@ type issuerConfigEntry struct {
DefaultFollowsLatestIssuer bool `json:"default_follows_latest_issuer"`
}
+type clusterConfigEntry struct {
+ Path string `json:"path"`
+}
+
+type aiaConfigEntry struct {
+ IssuingCertificates []string `json:"issuing_certificates"`
+ CRLDistributionPoints []string `json:"crl_distribution_points"`
+ OCSPServers []string `json:"ocsp_servers"`
+ EnableTemplating bool `json:"enable_templating"`
+}
+
+func (c *aiaConfigEntry) toURLEntries(sc *storageContext, issuer issuerID) (*certutil.URLEntries, error) {
+ if len(c.IssuingCertificates) == 0 && len(c.CRLDistributionPoints) == 0 && len(c.OCSPServers) == 0 {
+ return &certutil.URLEntries{}, nil
+ }
+
+ result := certutil.URLEntries{
+ IssuingCertificates: c.IssuingCertificates[:],
+ CRLDistributionPoints: c.CRLDistributionPoints[:],
+ OCSPServers: c.OCSPServers[:],
+ }
+
+ if c.EnableTemplating {
+ cfg, err := sc.getClusterConfig()
+ if err != nil {
+ return nil, fmt.Errorf("error fetching cluster-local address config: %w", err)
+ }
+
+ for name, source := range map[string]*[]string{
+ "issuing_certificates": &result.IssuingCertificates,
+ "crl_distribution_points": &result.CRLDistributionPoints,
+ "ocsp_servers": &result.OCSPServers,
+ } {
+ templated := make([]string, len(*source))
+ for index, uri := range *source {
+ if strings.Contains(uri, "{{cluster_path}}") && len(cfg.Path) == 0 {
+ return nil, fmt.Errorf("unable to template AIA URLs as we lack local cluster address information")
+ }
+
+ if strings.Contains(uri, "{{issuer_id}}") && len(issuer) == 0 {
+ // Elide issuer AIA info as we lack an issuer_id.
+ return nil, fmt.Errorf("unable to template AIA URLs as we lack an issuer_id for this operation")
+ }
+
+ uri = strings.ReplaceAll(uri, "{{cluster_path}}", cfg.Path)
+ uri = strings.ReplaceAll(uri, "{{issuer_id}}", issuer.String())
+ templated[index] = uri
+ }
+
+ if uri := validateURLs(templated); uri != "" {
+ return nil, fmt.Errorf("error validating templated %v; invalid URI: %v", name, uri)
+ }
+
+ *source = templated
+ }
+ }
+
+ return &result, nil
+}
+
type storageContext struct {
Context context.Context
Storage logical.Storage
@@ -506,17 +567,26 @@ func (i issuerEntry) CanMaybeSignWithAlgo(algo x509.SignatureAlgorithm) error {
return fmt.Errorf("unable to use issuer of type %v to sign with %v key type", cert.PublicKeyAlgorithm.String(), algo.String())
}
-func (i issuerEntry) GetAIAURLs(sc *storageContext) (urls *certutil.URLEntries, err error) {
+func (i issuerEntry) GetAIAURLs(sc *storageContext) (*certutil.URLEntries, error) {
// Default to the per-issuer AIA URLs.
- urls = i.AIAURIs
+ entries := i.AIAURIs
// If none are set (either due to a nil entry or because no URLs have
// been provided), fall back to the global AIA URL config.
- if urls == nil || (len(urls.IssuingCertificates) == 0 && len(urls.CRLDistributionPoints) == 0 && len(urls.OCSPServers) == 0) {
- urls, err = getGlobalAIAURLs(sc.Context, sc.Storage)
+ if entries == nil || (len(entries.IssuingCertificates) == 0 && len(entries.CRLDistributionPoints) == 0 && len(entries.OCSPServers) == 0) {
+ var err error
+
+ entries, err = getGlobalAIAURLs(sc.Context, sc.Storage)
+ if err != nil {
+ return nil, err
+ }
}
- return urls, err
+ if entries == nil {
+ return &certutil.URLEntries{}, nil
+ }
+
+ return entries.toURLEntries(sc, i.ID)
}
func (sc *storageContext) listIssuers() ([]issuerID, error) {
@@ -1246,3 +1316,30 @@ func (sc *storageContext) listRevokedCerts() ([]string, error) {
return list, err
}
+
+func (sc *storageContext) getClusterConfig() (*clusterConfigEntry, error) {
+ entry, err := sc.Storage.Get(sc.Context, clusterConfigPath)
+ if err != nil {
+ return nil, err
+ }
+
+ var result clusterConfigEntry
+ if entry == nil {
+ return &result, nil
+ }
+
+ if err = entry.DecodeJSON(&result); err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+func (sc *storageContext) writeClusterConfig(config *clusterConfigEntry) error {
+ entry, err := logical.StorageEntryJSON(clusterConfigPath, config)
+ if err != nil {
+ return err
+ }
+
+ return sc.Storage.Put(sc.Context, entry)
+}
diff --git a/builtin/logical/pki/storage_migrations_test.go b/builtin/logical/pki/storage_migrations_test.go
index ef1b59e5f..65678f418 100644
--- a/builtin/logical/pki/storage_migrations_test.go
+++ b/builtin/logical/pki/storage_migrations_test.go
@@ -483,8 +483,7 @@ func TestExpectedOpsWork_PreMigration(t *testing.T) {
},
MountPoint: "pki/",
})
- require.NoError(t, err, "error setting URL config")
- require.Nil(t, resp, "got non-nil response setting URL config")
+ requireSuccessNonNilResponse(t, resp, err)
// Make sure we can fetch the old values...
for _, path := range []string{"ca/pem", "ca_chain", "cert/" + serialNum, "cert/ca", "cert/crl", "cert/ca_chain", "config/crl", "config/urls"} {
diff --git a/changelog/18199.txt b/changelog/18199.txt
new file mode 100644
index 000000000..706a5568c
--- /dev/null
+++ b/changelog/18199.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+secrets/pki: Allow templating performance replication cluster- and issuer-specific AIA URLs.
+```
diff --git a/website/content/api-docs/secret/pki.mdx b/website/content/api-docs/secret/pki.mdx
index dfe56b6d0..d640e7443 100644
--- a/website/content/api-docs/secret/pki.mdx
+++ b/website/content/api-docs/secret/pki.mdx
@@ -65,6 +65,8 @@ update your API calls accordingly.
- [Set Issuers Configuration](#set-issuers-configuration)
- [Read Keys Configuration](#read-keys-configuration)
- [Set Keys Configuration](#set-keys-configuration)
+ - [Read Cluster Configuration](#read-cluster-configuration)
+ - [Set Cluster Configuration](#set-cluster-configuration)
- [Read CRL Configuration](#read-crl-configuration)
- [Set CRL Configuration](#set-crl-configuration)
- [Rotate CRLs](#rotate-crls)
@@ -2159,6 +2161,16 @@ do so, import a new issuer and a new `issuer_id` will be assigned.
[RFC 5280 Section 4.2.2.1](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.2.1)
for information about the Authority Information Access field.
+- `enable_aia_url_templating` `(bool: false)` - Specifies that the above AIA
+ URL values (`issuing_certificates`, `crl_distribution_points`, and
+ `ocsp_servers`) should be templated. This replaces the literal value
+ `{{issuer_id}}` with the ID of the issuer doing the issuance, and the
+ literal value `{{cluster_path}}` with the value of `path` from the
+ cluster-local configuration endpoint `/config/cluster`.
+
+~> **Note**: If no cluster-local address is present and templating is used,
+ issuance will fail.
+
#### Sample Payload
```json
@@ -2898,7 +2910,11 @@ parameter.
suggested to use the per-issuer AIA information instead of the global
AIA information. If any of the per-issuer AIA fields are set, the entire
issuer's preferences will be used instead. Otherwise, these fields are used
- as a fallback.
+ as a fallback.
+ This can be achieved by using _templated_ global AIA values, but setting
+ the cluster-local address in configuration. When used, this value **must**
+ be set on all performance replication clusters, otherwise issuance will
+ fail!
#### Parameters
@@ -2925,6 +2941,23 @@ parameter.
[RFC 5280 Section 4.2.2.1](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.2.1)
for information about the Authority Information Access field.
+- `enable_templating` `(bool: false)` - Specifies that the above AIA
+ URL values (`issuing_certificates`, `crl_distribution_points`, and
+ `ocsp_servers`) should be templated. This replaces the literal value
+ `{{issuer_id}}` with the ID of the issuer doing the issuance, and the
+ literal value `{{cluster_path}}` with the value of `path` from the
+ cluster-local configuration endpoint `/config/cluster`.
+
+ For example, the following values can be used globally to ensure all AIA
+ URIs use the cluster-local, per-issuer canonical reference:
+
+ - `issuing_certificates={{cluster_path}}/issuer/{{issuer_id}}/der`
+ - `crl_distribution_points={{cluster_path}}/issuer/{{issuer_id}}/crl/der`
+ - `ocsp_servers={{cluster_path}}/ocsp`
+
+~> **Note**: If no cluster-local address is present and templating is used,
+ issuance will fail.
+
#### Sample Payload
```json
@@ -3101,6 +3134,77 @@ $ curl \
}
```
+### Read Cluster Configuration
+
+This endpoint fetches the cluster-local configuration.
+
+Presently the only cluster-local config option is `path`, which sets the URL
+to this mount on a particular performance replication cluster. This is useful
+for populating `{{cluster_path}}` for AIA URL templating, but may be used for
+other uses in the future.
+
+| Method | Path |
+| :----- | :-------------------- |
+| `GET` | `/pki/config/cluster` |
+
+#### Sample Request
+
+```shell-session
+$ curl \
+ --header "X-Vault-Token: ..." \
+ http://127.0.0.1:8200/v1/pki/config/cluster
+```
+
+#### Sample Response
+
+```json
+{
+ "lease_id": "",
+ "renewable": false,
+ "lease_duration": 0,
+ "data": {
+ "path": ""
+ },
+ "auth": null
+}
+```
+
+### Set Cluster Configuration
+
+This endpoint sets cluster-local configuration.
+
+Presently the only cluster-local config option is `path`, which sets the URL
+to this mount on a particular performance replication cluster. This is useful
+for AIA URL templating.
+
+| Method | Path |
+| :----- | :-------------------- |
+| `POST` | `/pki/config/cluster` |
+
+#### Parameters
+
+- `path` `(string: "")` - Specifies the path to this performance replication
+ cluster's API mount path, including any namespaces as path components.
+ For example, `https://pr-a.vault.example.com/v1/ns1/pki-root`.
+
+#### Sample Payload
+
+```json
+{
+ "path": ["https://..."]
+}
+```
+
+#### Sample Request
+
+```shell-session
+$ curl \
+ --header "X-Vault-Token: ..." \
+ --request POST \
+ --data @payload.json \
+ http://127.0.0.1:8200/v1/pki/config/cluster
+```
+
### Read CRL Configuration
This endpoint allows getting the duration for which the generated CRL should be