Use duration and renewBefore to control API cert rotation

These configuration knobs are much more human-understandable than the
previous percentage-based threshold flag.

We now allow users to set the lifetime of the serving cert via a ConfigMap.
Previously this was hardcoded to 1 year.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Andrew Keesler
2020-08-20 16:35:04 -04:00
parent 3929fa672e
commit 39c299a32d
14 changed files with 190 additions and 136 deletions
+25 -1
View File
@@ -9,9 +9,10 @@ package api
type Config struct {
WebhookConfig WebhookConfigSpec `json:"webhook"`
DiscoveryInfo DiscoveryInfoSpec `json:"discovery"`
APIConfig APIConfigSpec `json:"api"`
}
// WebhookConfig contains configuration knobs specific to Pinniped's use
// WebhookConfig contains configuration knobs specific to pinniped's use
// of a webhook for token validation.
type WebhookConfigSpec struct {
// URL contains the URL of the webhook that pinniped will use
@@ -31,3 +32,26 @@ type DiscoveryInfoSpec struct {
// URL contains the URL at which pinniped can be contacted.
URL *string `json:"url,omitempty"`
}
// APIConfigSpec contains configuration knobs for the pinniped API.
//nolint: golint
type APIConfigSpec struct {
ServingCertificateConfig ServingCertificateConfigSpec `json:"servingCertificate"`
}
// ServingCertificateConfigSpec contains the configuration knobs for the API's
// serving certificate, i.e., the x509 certificate that it uses for the server
// certificate in inbound TLS connections.
type ServingCertificateConfigSpec struct {
// DurationSeconds is the validity period, in seconds, of the API serving
// certificate. By default, the serving certificate is issued for 31536000
// seconds (1 year).
DurationSeconds *int64 `json:"durationSeconds,omitempty"`
// RenewBeforeSeconds is the period of time, in seconds, that pinniped will
// wait before rotating the serving certificate. This period of time starts
// upon issuance of the serving certificate. This must be less than
// DurationSeconds. By default, pinniped begins rotation after 23328000
// seconds (about 9 months).
RenewBeforeSeconds *int64 `json:"renewBeforeSeconds,omitempty"`
}
+37 -1
View File
@@ -13,10 +13,18 @@ import (
"sigs.k8s.io/yaml"
"github.com/suzerain-io/pinniped/internal/constable"
"github.com/suzerain-io/pinniped/pkg/config/api"
)
// FromPath loads an api.Config from a provided local file path.
const (
aboutAYear = 60 * 60 * 24 * 365
about9Months = 60 * 60 * 24 * 30 * 9
)
// FromPath loads an api.Config from a provided local file path, inserts any
// defaults (from the api.Config documentation), and verifies that the config is
// valid (per the api.Config documentation).
//
// Note! The api.Config file should contain base64-encoded WebhookCABundle data.
// This function will decode that base64-encoded data to PEM bytes to be stored
@@ -32,5 +40,33 @@ func FromPath(path string) (*api.Config, error) {
return nil, fmt.Errorf("decode yaml: %w", err)
}
maybeSetAPIDefaults(&config.APIConfig)
if err := validateAPI(&config.APIConfig); err != nil {
return nil, fmt.Errorf("validate api: %w", err)
}
return &config, nil
}
func maybeSetAPIDefaults(apiConfig *api.APIConfigSpec) {
if apiConfig.ServingCertificateConfig.DurationSeconds == nil {
apiConfig.ServingCertificateConfig.DurationSeconds = int64Ptr(aboutAYear)
}
if apiConfig.ServingCertificateConfig.RenewBeforeSeconds == nil {
apiConfig.ServingCertificateConfig.RenewBeforeSeconds = int64Ptr(about9Months)
}
}
func validateAPI(apiConfig *api.APIConfigSpec) error {
if *apiConfig.ServingCertificateConfig.DurationSeconds < *apiConfig.ServingCertificateConfig.RenewBeforeSeconds {
return constable.Error("durationSeconds cannot be smaller than renewBeforeSeconds")
}
return nil
}
func int64Ptr(i int64) *int64 {
return &i
}
+27 -7
View File
@@ -18,6 +18,7 @@ func TestFromPath(t *testing.T) {
name string
path string
wantConfig *api.Config
wantError string
}{
{
name: "Happy",
@@ -30,11 +31,17 @@ func TestFromPath(t *testing.T) {
URL: "https://tuna.com/fish?marlin",
CABundle: []byte("-----BEGIN CERTIFICATE-----..."),
},
APIConfig: api.APIConfigSpec{
ServingCertificateConfig: api.ServingCertificateConfigSpec{
DurationSeconds: int64Ptr(3600),
RenewBeforeSeconds: int64Ptr(2400),
},
},
},
},
{
name: "NoDiscovery",
path: "testdata/no-discovery.yaml",
name: "Default",
path: "testdata/default.yaml",
wantConfig: &api.Config{
DiscoveryInfo: api.DiscoveryInfoSpec{
URL: nil,
@@ -43,21 +50,34 @@ func TestFromPath(t *testing.T) {
URL: "https://tuna.com/fish?marlin",
CABundle: []byte("-----BEGIN CERTIFICATE-----..."),
},
APIConfig: api.APIConfigSpec{
ServingCertificateConfig: api.ServingCertificateConfigSpec{
DurationSeconds: int64Ptr(60 * 60 * 24 * 365), // about a year
RenewBeforeSeconds: int64Ptr(60 * 60 * 24 * 30 * 9), // about 9 months
},
},
},
},
{
name: "InvalidDurationRenewBefore",
path: "testdata/invalid-duration-renew-before.yaml",
wantError: "validate api: durationSeconds cannot be smaller than renewBeforeSeconds",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
config, err := FromPath(test.path)
require.NoError(t, err)
require.Equal(t, test.wantConfig, config)
if test.wantError != "" {
require.EqualError(t, err, test.wantError)
} else {
require.NoError(t, err)
require.Equal(t, test.wantConfig, config)
}
})
}
}
func stringPtr(s string) *string {
sPtr := new(string)
*sPtr = s
return sPtr
return &s
}
+4
View File
@@ -4,3 +4,7 @@ discovery:
webhook:
url: https://tuna.com/fish?marlin
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tLi4u
api:
servingCertificate:
durationSeconds: 3600
renewBeforeSeconds: 2400
@@ -0,0 +1,8 @@
---
webhook:
url: https://tuna.com/fish?marlin
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tLi4u
api:
servingCertificate:
durationSeconds: 2400
renewBeforeSeconds: 3600