Make PKI root generation idempotent-ish and add delete endpoint. (#3165)
This commit is contained in:
parent
8902240dfa
commit
e4eb6e9020
|
@ -20,5 +20,5 @@ branches:
|
|||
|
||||
script:
|
||||
- make bootstrap
|
||||
- travis_wait 30 make test
|
||||
- travis_wait 30 make testrace
|
||||
- travis_wait 45 make test
|
||||
- travis_wait 45 make testrace
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
## 0.8.1 (Unreleased)
|
||||
|
||||
DEPRECATIONS/CHANGES:
|
||||
|
||||
* PKI Root Generation: Calling `pki/root/generate` when a CA cert/key already
|
||||
exists will now return a `204` instead of overwriting an existing root. If
|
||||
you want to recreate the root, first run a delete operation on `pki/root`
|
||||
(requires `sudo` capability), then generate it again.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* auth/approle: Allow array input for policies in addition to comma-delimited
|
||||
|
|
|
@ -39,15 +39,20 @@ func Backend() *backend {
|
|||
"crl",
|
||||
"certs/",
|
||||
},
|
||||
|
||||
Root: []string{
|
||||
"root",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: []*framework.Path{
|
||||
pathListRoles(&b),
|
||||
pathRoles(&b),
|
||||
pathGenerateRoot(&b),
|
||||
pathSignIntermediate(&b),
|
||||
pathDeleteRoot(&b),
|
||||
pathGenerateIntermediate(&b),
|
||||
pathSetSignedIntermediate(&b),
|
||||
pathSignIntermediate(&b),
|
||||
pathConfigCA(&b),
|
||||
pathConfigCRL(&b),
|
||||
pathConfigURLs(&b),
|
||||
|
|
|
@ -22,10 +22,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/certutil"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
logicaltest "github.com/hashicorp/vault/logical/testing"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
|
@ -648,6 +651,11 @@ func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s
|
|||
ErrorOk: true,
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.DeleteOperation,
|
||||
Path: "root",
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "root/generate/exported",
|
||||
|
@ -865,6 +873,11 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
},
|
||||
|
||||
// Test a bunch of generation stuff
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.DeleteOperation,
|
||||
Path: "root",
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "root/generate/exported",
|
||||
|
@ -997,6 +1010,11 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
},
|
||||
|
||||
// Do it all again, with EC keys and DER format
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.DeleteOperation,
|
||||
Path: "root",
|
||||
},
|
||||
|
||||
logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "root/generate/exported",
|
||||
|
@ -1218,7 +1236,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
Operation: logical.ReadOperation,
|
||||
PreFlight: setSerialUnderTest,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
if resp != nil && resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
|
@ -1232,7 +1250,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
Operation: logical.ReadOperation,
|
||||
PreFlight: setSerialUnderTest,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
if resp != nil && resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
|
@ -1290,7 +1308,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
Operation: logical.ReadOperation,
|
||||
PreFlight: setSerialUnderTest,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
if resp != nil && resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
|
@ -1304,7 +1322,7 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
Operation: logical.ReadOperation,
|
||||
PreFlight: setSerialUnderTest,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
if resp != nil && resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
|
||||
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
|
@ -1330,8 +1348,8 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
Operation: logical.ReadOperation,
|
||||
PreFlight: setSerialUnderTest,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["error"] == nil || resp.Data["error"].(string) == "" {
|
||||
return fmt.Errorf("didn't get an expected error")
|
||||
if resp != nil {
|
||||
return fmt.Errorf("expected no response")
|
||||
}
|
||||
|
||||
serialUnderTest = "cert/" + reqdata["ec_int_serial_number"].(string)
|
||||
|
@ -1344,8 +1362,8 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
|
|||
Operation: logical.ReadOperation,
|
||||
PreFlight: setSerialUnderTest,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp.Data["error"] == nil || resp.Data["error"].(string) == "" {
|
||||
return fmt.Errorf("didn't get an expected error")
|
||||
if resp != nil {
|
||||
return fmt.Errorf("expected no response")
|
||||
}
|
||||
|
||||
serialUnderTest = "cert/" + reqdata["rsa_int_serial_number"].(string)
|
||||
|
@ -2156,6 +2174,96 @@ func TestBackend_SignVerbatim(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBackend_Root_Idempotentcy(t *testing.T) {
|
||||
coreConfig := &vault.CoreConfig{
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"pki": Factory,
|
||||
},
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
client := cluster.Cores[0].Client
|
||||
var err error
|
||||
err = client.Sys().Mount("pki", &api.MountInput{
|
||||
Type: "pki",
|
||||
Config: api.MountConfigInput{
|
||||
DefaultLeaseTTL: "16h",
|
||||
MaxLeaseTTL: "32h",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
|
||||
"common_name": "myvault.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatal("expected ca info")
|
||||
}
|
||||
resp, err = client.Logical().Read("pki/cert/ca_chain")
|
||||
r1Data := resp.Data
|
||||
|
||||
// Try again, make sure it's a 204 and same CA
|
||||
resp, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
|
||||
"common_name": "myvault.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("expected no ca info")
|
||||
}
|
||||
resp, err = client.Logical().Read("pki/cert/ca_chain")
|
||||
r2Data := resp.Data
|
||||
if !reflect.DeepEqual(r1Data, r2Data) {
|
||||
t.Fatal("got different ca certs")
|
||||
}
|
||||
|
||||
resp, err = client.Logical().Delete("pki/root")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("expected nil response")
|
||||
}
|
||||
// Make sure it behaves the same
|
||||
resp, err = client.Logical().Delete("pki/root")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("expected nil response")
|
||||
}
|
||||
|
||||
_, err = client.Logical().Read("pki/cert/ca_chain")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
resp, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
|
||||
"common_name": "myvault.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatal("expected ca info")
|
||||
}
|
||||
|
||||
_, err = client.Logical().Read("pki/cert/ca_chain")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
rsaCAKey string = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAmPQlK7xD5p+E8iLQ8XlVmll5uU2NKMxKY3UF5tbh+0vkc+Fy
|
||||
|
|
|
@ -16,9 +16,7 @@ func pathConfigCA(b *backend) *framework.Path {
|
|||
"pem_bundle": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `PEM-format, concatenated unencrypted
|
||||
secret key and certificate, or, if a
|
||||
CSR was generated with the "generate"
|
||||
endpoint, just the signed certificate.`,
|
||||
secret key and certificate.`,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData)
|
|||
caInfo, err := fetchCAInfo(req)
|
||||
switch err.(type) {
|
||||
case errutil.UserError:
|
||||
response = logical.ErrorResponse(funcErr.Error())
|
||||
response = logical.ErrorResponse(err.Error())
|
||||
goto reply
|
||||
case errutil.InternalError:
|
||||
retErr = err
|
||||
|
@ -189,7 +189,7 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData)
|
|||
}
|
||||
}
|
||||
if certEntry == nil {
|
||||
response = logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found", serial))
|
||||
response = nil
|
||||
goto reply
|
||||
}
|
||||
|
||||
|
@ -244,6 +244,11 @@ reply:
|
|||
}
|
||||
case retErr != nil:
|
||||
response = nil
|
||||
return
|
||||
case response == nil:
|
||||
return
|
||||
case response.IsError():
|
||||
return response, nil
|
||||
default:
|
||||
response.Data["certificate"] = string(certificate)
|
||||
response.Data["revocation_time"] = revocationTime
|
||||
|
|
|
@ -28,6 +28,21 @@ func pathGenerateRoot(b *backend) *framework.Path {
|
|||
return ret
|
||||
}
|
||||
|
||||
func pathDeleteRoot(b *backend) *framework.Path {
|
||||
ret := &framework.Path{
|
||||
Pattern: "root",
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.DeleteOperation: b.pathCADeleteRoot,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathDeleteRootHelpSyn,
|
||||
HelpDescription: pathDeleteRootHelpDesc,
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func pathSignIntermediate(b *backend) *framework.Path {
|
||||
ret := &framework.Path{
|
||||
Pattern: "root/sign-intermediate",
|
||||
|
@ -66,10 +81,23 @@ the non-repudiation flag.`,
|
|||
return ret
|
||||
}
|
||||
|
||||
func (b *backend) pathCADeleteRoot(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
return nil, req.Storage.Delete("config/ca_bundle")
|
||||
}
|
||||
|
||||
func (b *backend) pathCAGenerateRoot(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
var err error
|
||||
|
||||
entry, err := req.Storage.Get("config/ca_bundle")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
exported, format, role, errorResp := b.getGenerationParams(data)
|
||||
if errorResp != nil {
|
||||
return errorResp, nil
|
||||
|
@ -133,7 +161,7 @@ func (b *backend) pathCAGenerateRoot(
|
|||
}
|
||||
|
||||
// Store it as the CA bundle
|
||||
entry, err := logical.StorageEntryJSON("config/ca_bundle", cb)
|
||||
entry, err = logical.StorageEntryJSON("config/ca_bundle", cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -299,6 +327,14 @@ const pathGenerateRootHelpDesc = `
|
|||
See the API documentation for more information.
|
||||
`
|
||||
|
||||
const pathDeleteRootHelpSyn = `
|
||||
Deletes the root CA key to allow a new one to be generated.
|
||||
`
|
||||
|
||||
const pathDeleteRootHelpDesc = `
|
||||
See the API documentation for more information.
|
||||
`
|
||||
|
||||
const pathSignIntermediateHelpSyn = `
|
||||
Issue an intermediate CA certificate based on the provided CSR.
|
||||
`
|
||||
|
|
|
@ -39,6 +39,7 @@ update your API calls accordingly.
|
|||
* [List Roles](#list-roles)
|
||||
* [Delete Role](#delete-role)
|
||||
* [Generate Root](#generate-root)
|
||||
* [Delete Root](#delete-root)
|
||||
* [Sign Intermediate](#sign-intermediate)
|
||||
* [Sign Certificate](#sign-certificate)
|
||||
* [Sign Verbatim](#sign-verbatim)
|
||||
|
@ -47,8 +48,9 @@ update your API calls accordingly.
|
|||
## Read CA Certificate
|
||||
|
||||
This endpoint retrieves the CA certificate *in raw DER-encoded form*. This is a
|
||||
bare endpoint that does not return a standard Vault data structure. If `/pem` is
|
||||
added to the endpoint, the CA certificate is returned in PEM format.
|
||||
bare endpoint that does not return a standard Vault data structure and cannot
|
||||
be read by the Vault CLI. If `/pem` is added to the endpoint, the CA
|
||||
certificate is returned in PEM format.
|
||||
|
||||
This is an unauthenticated endpoint.
|
||||
|
||||
|
@ -73,7 +75,7 @@ $ curl \
|
|||
|
||||
This endpoint retrieves the CA certificate chain, including the CA _in PEM
|
||||
format_. This is a bare endpoint that does not return a standard Vault data
|
||||
structure.
|
||||
structure and cannot be read by the Vault CLI.
|
||||
|
||||
This is an unauthenticated endpoint.
|
||||
|
||||
|
@ -460,8 +462,6 @@ $ curl \
|
|||
https://vault.rocks/v1/pki/intermediate/generate/internal
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"lease_id": "",
|
||||
|
@ -882,14 +882,18 @@ $ curl \
|
|||
|
||||
## Generate Root
|
||||
|
||||
This endpoint generates a new self-signed CA certificate and private key. _This
|
||||
will overwrite any previously-existing private key and certificate._ If the path
|
||||
ends with `exported`, the private key will be returned in the response; if it is
|
||||
`internal` the private key will not be returned and *cannot be retrieved later*.
|
||||
Distribution points use the values set via `config/urls`.
|
||||
This endpoint generates a new self-signed CA certificate and private key. If
|
||||
the path ends with `exported`, the private key will be returned in the
|
||||
response; if it is `internal` the private key will not be returned and *cannot
|
||||
be retrieved later*. Distribution points use the values set via `config/urls`.
|
||||
|
||||
As with other issued certificates, Vault will automatically revoke the generated
|
||||
root at the end of its lease period; the CA certificate will sign its own CRL.
|
||||
As with other issued certificates, Vault will automatically revoke the
|
||||
generated root at the end of its lease period; the CA certificate will sign its
|
||||
own CRL.
|
||||
|
||||
As of Vault 0.8.1, if a CA cert/key already exists within the backend, this
|
||||
function will return a 204 and will not overwrite it. Previous versions of
|
||||
Vault would overwrite the existing cert/key with new values.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :--------------------------- | :--------------------- |
|
||||
|
@ -974,6 +978,26 @@ $ curl \
|
|||
}
|
||||
```
|
||||
|
||||
## Delete Root
|
||||
|
||||
This endpoint deletes the current CA key (the old CA certificate will still be
|
||||
accessible for reading until a new certificate/key are generated or uploaded).
|
||||
_This endpoint requires sudo/root privileges._
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :--------------------------- | :--------------------- |
|
||||
| `DELETE` | `/pki/root` | `204 (empty body)` |
|
||||
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request DELETE \
|
||||
https://vault.rocks/v1/pki/root
|
||||
```
|
||||
|
||||
## Sign Intermediate
|
||||
|
||||
This endpoint uses the configured CA certificate to issue a certificate with
|
||||
|
|
|
@ -61,8 +61,7 @@ $ curl \
|
|||
|
||||
## Configure Lease
|
||||
|
||||
This endpoint configures the lease settings for generated credentials. This is
|
||||
endpoint requires sudo privileges.
|
||||
This endpoint configures the lease settings for generated credentials.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :--------------------------- | :--------------------- |
|
||||
|
|
Loading…
Reference in New Issue