open-vault/command/pki_health_check_test.go
Steven Clark c8837f2010
Add ACME health checks to pki health-check CLI (#20619)
* Add ACME health checks to pki health-check CLI

 - Verify we have the required header values listed within allowed_response_headers: 'Replay-Nonce', 'Link', 'Location'
 - Make sure the local cluster config path variable contains an URL with an https scheme

* Split ACME health checks into two separate verifications

 - Promote ACME usage through the enable_acme_issuance check, if ACME is disabled currently
 - If ACME is enabled verify that we have a valid
    'path' field within local cluster configuration as well as the proper response headers allowed.
 - Factor out response header verifications into a separate check mainly to work around possible permission issues.

* Only recommend enabling ACME on mounts with intermediate issuers

* Attempt to connect to the ACME directory based on the cluster path variable

 - Final health check is to attempt to connect to the ACME directory based on the cluster local 'path' value. Only if we successfully connect do we say ACME is healthy.

* Fix broken unit test
2023-05-23 10:37:31 -04:00

695 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/healthcheck"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestPKIHC_AllGood(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
AuditNonHMACRequestKeys: healthcheck.VisibleReqParams,
AuditNonHMACResponseKeys: healthcheck.VisibleRespParams,
PassthroughRequestHeaders: []string{"If-Modified-Since"},
AllowedResponseHeaders: []string{"Last-Modified", "Replay-Nonce", "Link", "Location"},
MaxLeaseTTL: "36500d",
},
}); err != nil {
t.Fatalf("pki mount error: %#v", err)
}
if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"key_type": "ec",
"common_name": "Root X1",
"ttl": "3650d",
}); err != nil || resp == nil {
t.Fatalf("failed to prime CA: %v", err)
}
if _, err := client.Logical().Read("pki/crl/rotate"); err != nil {
t.Fatalf("failed to rotate CRLs: %v", err)
}
if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{
"allow_any_name": true,
"no_store": true,
}); err != nil {
t.Fatalf("failed to write role: %v", err)
}
if _, err := client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{
"enabled": true,
"tidy_cert_store": true,
}); err != nil {
t.Fatalf("failed to write auto-tidy config: %v", err)
}
if _, err := client.Logical().Write("pki/tidy", map[string]interface{}{
"tidy_cert_store": true,
}); err != nil {
t.Fatalf("failed to run tidy: %v", err)
}
path, err := url.Parse(client.Address())
require.NoError(t, err, "failed parsing client address")
if _, err := client.Logical().Write("pki/config/cluster", map[string]interface{}{
"path": path.JoinPath("/v1/", "pki/").String(),
}); err != nil {
t.Fatalf("failed to update local cluster: %v", err)
}
if _, err := client.Logical().Write("pki/config/acme", map[string]interface{}{
"enabled": "true",
}); err != nil {
t.Fatalf("failed to update acme config: %v", err)
}
_, _, results := execPKIHC(t, client, true)
validateExpectedPKIHC(t, expectedAllGood, results)
}
func TestPKIHC_AllBad(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
}); err != nil {
t.Fatalf("pki mount error: %#v", err)
}
if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"key_type": "ec",
"common_name": "Root X1",
"ttl": "35d",
}); err != nil || resp == nil {
t.Fatalf("failed to prime CA: %v", err)
}
if _, err := client.Logical().Write("pki/config/crl", map[string]interface{}{
"expiry": "5s",
}); err != nil {
t.Fatalf("failed to issue leaf cert: %v", err)
}
if _, err := client.Logical().Read("pki/crl/rotate"); err != nil {
t.Fatalf("failed to rotate CRLs: %v", err)
}
time.Sleep(5 * time.Second)
if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{
"allow_localhost": true,
"allowed_domains": "*.example.com",
"allow_glob_domains": true,
"allow_wildcard_certificates": true,
"no_store": false,
"key_type": "ec",
"ttl": "30d",
}); err != nil {
t.Fatalf("failed to write role: %v", err)
}
if _, err := client.Logical().Write("pki/issue/testing", map[string]interface{}{
"common_name": "something.example.com",
}); err != nil {
t.Fatalf("failed to issue leaf cert: %v", err)
}
if _, err := client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{
"enabled": false,
"tidy_cert_store": false,
}); err != nil {
t.Fatalf("failed to write auto-tidy config: %v", err)
}
_, _, results := execPKIHC(t, client, true)
validateExpectedPKIHC(t, expectedAllBad, results)
}
func TestPKIHC_OnlyIssuer(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
}); err != nil {
t.Fatalf("pki mount error: %#v", err)
}
if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"key_type": "ec",
"common_name": "Root X1",
"ttl": "35d",
}); err != nil || resp == nil {
t.Fatalf("failed to prime CA: %v", err)
}
_, _, results := execPKIHC(t, client, true)
validateExpectedPKIHC(t, expectedEmptyWithIssuer, results)
}
func TestPKIHC_NoMount(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
code, message, _ := execPKIHC(t, client, false)
if code != 1 {
t.Fatalf("Expected return code 1 from invocation on non-existent mount, got %v\nOutput: %v", code, message)
}
if !strings.Contains(message, "route entry not found") {
t.Fatalf("Expected failure to talk about missing route entry, got exit code %v\nOutput: %v", code, message)
}
}
func TestPKIHC_ExpectedEmptyMount(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
}); err != nil {
t.Fatalf("pki mount error: %#v", err)
}
code, message, _ := execPKIHC(t, client, false)
if code != 1 {
t.Fatalf("Expected return code 1 from invocation on empty mount, got %v\nOutput: %v", code, message)
}
if !strings.Contains(message, "lacks any configured issuers,") {
t.Fatalf("Expected failure to talk about no issuers, got exit code %v\nOutput: %v", code, message)
}
}
func TestPKIHC_NoPerm(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
}); err != nil {
t.Fatalf("pki mount error: %#v", err)
}
if resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"key_type": "ec",
"common_name": "Root X1",
"ttl": "35d",
}); err != nil || resp == nil {
t.Fatalf("failed to prime CA: %v", err)
}
if _, err := client.Logical().Write("pki/config/crl", map[string]interface{}{
"expiry": "5s",
}); err != nil {
t.Fatalf("failed to issue leaf cert: %v", err)
}
if _, err := client.Logical().Read("pki/crl/rotate"); err != nil {
t.Fatalf("failed to rotate CRLs: %v", err)
}
time.Sleep(5 * time.Second)
if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{
"allow_localhost": true,
"allowed_domains": "*.example.com",
"allow_glob_domains": true,
"allow_wildcard_certificates": true,
"no_store": false,
"key_type": "ec",
"ttl": "30d",
}); err != nil {
t.Fatalf("failed to write role: %v", err)
}
if _, err := client.Logical().Write("pki/issue/testing", map[string]interface{}{
"common_name": "something.example.com",
}); err != nil {
t.Fatalf("failed to issue leaf cert: %v", err)
}
if _, err := client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{
"enabled": false,
"tidy_cert_store": false,
}); err != nil {
t.Fatalf("failed to write auto-tidy config: %v", err)
}
// Remove client token.
client.ClearToken()
_, _, results := execPKIHC(t, client, true)
validateExpectedPKIHC(t, expectedNoPerm, results)
}
func testPKIHealthCheckCommand(tb testing.TB) (*cli.MockUi, *PKIHealthCheckCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PKIHealthCheckCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func execPKIHC(t *testing.T, client *api.Client, ok bool) (int, string, map[string][]map[string]interface{}) {
t.Helper()
stdout := bytes.NewBuffer(nil)
stderr := bytes.NewBuffer(nil)
runOpts := &RunOptions{
Stdout: stdout,
Stderr: stderr,
Client: client,
}
code := RunCustom([]string{"pki", "health-check", "-format=json", "pki"}, runOpts)
combined := stdout.String() + stderr.String()
var results map[string][]map[string]interface{}
if err := json.Unmarshal([]byte(combined), &results); err != nil {
if ok {
t.Fatalf("failed to decode json (ret %v): %v\njson:\n%v", code, err, combined)
}
}
t.Log(combined)
return code, combined, results
}
func validateExpectedPKIHC(t *testing.T, expected, results map[string][]map[string]interface{}) {
t.Helper()
for test, subtest := range expected {
actual, ok := results[test]
require.True(t, ok, fmt.Sprintf("expected top-level test %v to be present", test))
if subtest == nil {
continue
}
require.NotNil(t, actual, fmt.Sprintf("expected top-level test %v to be non-empty; wanted wireframe format %v", test, subtest))
require.Equal(t, len(subtest), len(actual), fmt.Sprintf("top-level test %v has different number of results %v in wireframe, %v in test output\nwireframe: %v\noutput: %v\n", test, len(subtest), len(actual), subtest, actual))
for index, subset := range subtest {
for key, value := range subset {
a_value, present := actual[index][key]
require.True(t, present)
if value != nil {
require.Equal(t, value, a_value, fmt.Sprintf("in test: %v / result %v - when validating key %v\nWanted: %v\nGot: %v", test, index, key, subset, actual[index]))
}
}
}
}
for name := range results {
if _, present := expected[name]; !present {
t.Fatalf("got unexpected health check: %v\n%v", name, results[name])
}
}
}
var expectedAllGood = map[string][]map[string]interface{}{
"ca_validity_period": {
{
"status": "ok",
},
},
"crl_validity_period": {
{
"status": "ok",
},
{
"status": "ok",
},
},
"allow_acme_headers": {
{
"status": "ok",
},
},
"allow_if_modified_since": {
{
"status": "ok",
},
},
"audit_visibility": {
{
"status": "ok",
},
},
"enable_acme_issuance": {
{
"status": "ok",
},
},
"enable_auto_tidy": {
{
"status": "ok",
},
},
"role_allows_glob_wildcards": {
{
"status": "ok",
},
},
"role_allows_localhost": {
{
"status": "ok",
},
},
"role_no_store_false": {
{
"status": "ok",
},
},
"root_issued_leaves": {
{
"status": "ok",
},
},
"tidy_last_run": {
{
"status": "ok",
},
},
"too_many_certs": {
{
"status": "ok",
},
},
}
var expectedAllBad = map[string][]map[string]interface{}{
"ca_validity_period": {
{
"status": "critical",
},
},
"crl_validity_period": {
{
"status": "critical",
},
{
"status": "critical",
},
},
"allow_acme_headers": {
{
"status": "not_applicable",
},
},
"allow_if_modified_since": {
{
"status": "informational",
},
},
"audit_visibility": {
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
{
"status": "informational",
},
},
"enable_acme_issuance": {
{
"status": "not_applicable",
},
},
"enable_auto_tidy": {
{
"status": "informational",
},
},
"role_allows_glob_wildcards": {
{
"status": "warning",
},
},
"role_allows_localhost": {
{
"status": "warning",
},
},
"role_no_store_false": {
{
"status": "warning",
},
},
"root_issued_leaves": {
{
"status": "warning",
},
},
"tidy_last_run": {
{
"status": "critical",
},
},
"too_many_certs": {
{
"status": "ok",
},
},
}
var expectedEmptyWithIssuer = map[string][]map[string]interface{}{
"ca_validity_period": {
{
"status": "critical",
},
},
"crl_validity_period": {
{
"status": "ok",
},
{
"status": "ok",
},
},
"allow_acme_headers": {
{
"status": "not_applicable",
},
},
"allow_if_modified_since": nil,
"audit_visibility": nil,
"enable_acme_issuance": {
{
"status": "not_applicable",
},
},
"enable_auto_tidy": {
{
"status": "informational",
},
},
"role_allows_glob_wildcards": nil,
"role_allows_localhost": nil,
"role_no_store_false": nil,
"root_issued_leaves": {
{
"status": "ok",
},
},
"tidy_last_run": {
{
"status": "critical",
},
},
"too_many_certs": {
{
"status": "ok",
},
},
}
var expectedNoPerm = map[string][]map[string]interface{}{
"ca_validity_period": {
{
"status": "critical",
},
},
"crl_validity_period": {
{
"status": "insufficient_permissions",
},
{
"status": "critical",
},
{
"status": "critical",
},
},
"allow_acme_headers": {
{
"status": "insufficient_permissions",
},
},
"allow_if_modified_since": nil,
"audit_visibility": nil,
"enable_acme_issuance": {
{
"status": "insufficient_permissions",
},
},
"enable_auto_tidy": {
{
"status": "insufficient_permissions",
},
},
"role_allows_glob_wildcards": {
{
"status": "insufficient_permissions",
},
},
"role_allows_localhost": {
{
"status": "insufficient_permissions",
},
},
"role_no_store_false": {
{
"status": "insufficient_permissions",
},
},
"root_issued_leaves": {
{
"status": "insufficient_permissions",
},
},
"tidy_last_run": {
{
"status": "insufficient_permissions",
},
},
"too_many_certs": {
{
"status": "insufficient_permissions",
},
},
}