Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: periodic key revocation #401

Merged
merged 18 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ const (
// the CPU time it takes to read the archive over and over again. It is somewhat arbitrary.
// It likely doesn't make sense to use an interval this frequent in production use-cases.
minimumFileDataSourceMonitoringInterval = 100 * time.Millisecond
// This minimum was chosen to protect the host system from unnecessary work, while also allowing expired
// credentials to be revoked nearly instantaneously. It is not necessarily a recommendation.
// It likely doesn't make sense to use an interval this frequent in production use-cases.
minimumCredentialCleanupInterval = 100 * time.Millisecond
)

// DefaultLoggers is the default logging configuration used by Relay.
Expand Down Expand Up @@ -134,24 +138,25 @@ type Config struct {
// variables, individual fields are not documented here; instead, see the `README.md` section on
// configuration.
type MainConfig struct {
ExitOnError bool `conf:"EXIT_ON_ERROR"`
ExitAlways bool `conf:"EXIT_ALWAYS"`
IgnoreConnectionErrors bool `conf:"IGNORE_CONNECTION_ERRORS"`
StreamURI ct.OptURLAbsolute `conf:"STREAM_URI"`
BaseURI ct.OptURLAbsolute `conf:"BASE_URI"`
ClientSideBaseURI ct.OptURLAbsolute `conf:"CLIENT_SIDE_BASE_URI"`
Port ct.OptIntGreaterThanZero `conf:"PORT"`
InitTimeout ct.OptDuration `conf:"INIT_TIMEOUT"`
HeartbeatInterval ct.OptDuration `conf:"HEARTBEAT_INTERVAL"`
MaxClientConnectionTime ct.OptDuration `conf:"MAX_CLIENT_CONNECTION_TIME"`
DisconnectedStatusTime ct.OptDuration `conf:"DISCONNECTED_STATUS_TIME"`
TLSEnabled bool `conf:"TLS_ENABLED"`
TLSCert string `conf:"TLS_CERT"`
TLSKey string `conf:"TLS_KEY"`
TLSMinVersion OptTLSVersion `conf:"TLS_MIN_VERSION"`
LogLevel OptLogLevel `conf:"LOG_LEVEL"`
BigSegmentsStaleAsDegraded bool `conf:"BIG_SEGMENTS_STALE_AS_DEGRADED"`
BigSegmentsStaleThreshold ct.OptDuration `conf:"BIG_SEGMENTS_STALE_THRESHOLD"`
ExitOnError bool `conf:"EXIT_ON_ERROR"`
ExitAlways bool `conf:"EXIT_ALWAYS"`
IgnoreConnectionErrors bool `conf:"IGNORE_CONNECTION_ERRORS"`
StreamURI ct.OptURLAbsolute `conf:"STREAM_URI"`
BaseURI ct.OptURLAbsolute `conf:"BASE_URI"`
ClientSideBaseURI ct.OptURLAbsolute `conf:"CLIENT_SIDE_BASE_URI"`
Port ct.OptIntGreaterThanZero `conf:"PORT"`
InitTimeout ct.OptDuration `conf:"INIT_TIMEOUT"`
HeartbeatInterval ct.OptDuration `conf:"HEARTBEAT_INTERVAL"`
MaxClientConnectionTime ct.OptDuration `conf:"MAX_CLIENT_CONNECTION_TIME"`
DisconnectedStatusTime ct.OptDuration `conf:"DISCONNECTED_STATUS_TIME"`
TLSEnabled bool `conf:"TLS_ENABLED"`
TLSCert string `conf:"TLS_CERT"`
TLSKey string `conf:"TLS_KEY"`
TLSMinVersion OptTLSVersion `conf:"TLS_MIN_VERSION"`
LogLevel OptLogLevel `conf:"LOG_LEVEL"`
BigSegmentsStaleAsDegraded bool `conf:"BIG_SEGMENTS_STALE_AS_DEGRADED"`
BigSegmentsStaleThreshold ct.OptDuration `conf:"BIG_SEGMENTS_STALE_THRESHOLD"`
ExpiredCredentialCleanupInterval ct.OptDuration `conf:"EXPIRED_CREDENTIAL_CLEANUP_INTERVAL"`
}

// AutoConfigConfig contains configuration parameters for the auto-configuration feature.
Expand Down
45 changes: 13 additions & 32 deletions config/config_field_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"fmt"
"strings"

"github.com/launchdarkly/ld-relay/v8/internal/credential"

"github.com/launchdarkly/go-sdk-common/v3/ldlog"
)

Expand Down Expand Up @@ -44,6 +42,13 @@ type FilterKey string
// DefaultFilter represents the lack of a filter, meaning a full LaunchDarkly environment.
const DefaultFilter = FilterKey("")

func last4Chars(s string) string {
if len(s) < 4 { // COVERAGE: doesn't happen in unit tests, also can't happen with real environments
return s
}
return s[len(s)-4:]
}

// GetAuthorizationHeaderValue for SDKKey returns the same string, since SDK keys are passed in
// the Authorization header.
func (k SDKKey) GetAuthorizationHeaderValue() string {
Expand All @@ -57,21 +62,7 @@ func (k SDKKey) Defined() bool {
func (k SDKKey) String() string {
return string(k)
}

func (k SDKKey) Compare(cr credential.AutoConfig) (credential.SDKCredential, credential.Status) {
if cr.SDKKey == k {
return nil, credential.Unchanged
}
if cr.ExpiringSDKKey == k {
// If the AutoConfig update contains an ExpiringSDKKey that is equal to *this* key, then it means
// this key is now considered deprecated.
return cr.SDKKey, credential.Deprecated
} else {
// Otherwise if the AutoConfig update contains *some other* key, then it means this one must be considered
// expired.
return cr.SDKKey, credential.Expired
}
}
func (k SDKKey) Masked() string { return "..." + last4Chars(k.String()) }

// GetAuthorizationHeaderValue for MobileKey returns the same string, since mobile keys are passed in the
// Authorization header.
Expand All @@ -87,12 +78,7 @@ func (k MobileKey) String() string {
return string(k)
}

func (k MobileKey) Compare(cr credential.AutoConfig) (credential.SDKCredential, credential.Status) {
if cr.MobileKey == k {
return nil, credential.Unchanged
}
return cr.MobileKey, credential.Expired
}
func (k MobileKey) Masked() string { return "..." + last4Chars(k.String()) }

// GetAuthorizationHeaderValue for EnvironmentID returns an empty string, since environment IDs are not
// passed in a header but as part of the request URL.
Expand All @@ -108,10 +94,8 @@ func (k EnvironmentID) String() string {
return string(k)
}

func (k EnvironmentID) Compare(_ credential.AutoConfig) (credential.SDKCredential, credential.Status) {
// EnvironmentIDs should not change.
return nil, credential.Unchanged
}
// Masked is an alias for String(), because EnvironmentIDs are considered non-sensitive public information.
func (k EnvironmentID) Masked() string { return k.String() }

// GetAuthorizationHeaderValue for AutoConfigKey returns the same string, since these keys are passed in
// the Authorization header. Note that unlike the other kinds of authorization keys, this one is never
Expand All @@ -120,15 +104,12 @@ func (k AutoConfigKey) GetAuthorizationHeaderValue() string {
return string(k)
}

func (k AutoConfigKey) Compare(_ credential.AutoConfig) (credential.SDKCredential, credential.Status) {
// AutoConfigKeys should not change.
return nil, credential.Unchanged
}

func (k AutoConfigKey) String() string {
return string(k)
}

func (k AutoConfigKey) Masked() string { return last4Chars(string(k)) }

// UnmarshalText allows the SDKKey type to be set from environment variables.
func (k *SDKKey) UnmarshalText(data []byte) error {
*k = SDKKey(string(data))
Expand Down
10 changes: 10 additions & 0 deletions config/config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var (
errAutoConfWithFilters = errors.New("cannot configure filters if auto-configuration is enabled")
errMissingProjKey = errors.New("when filters are configured, all environments must specify a 'projKey'")
errInvalidFileDataSourceMonitoringInterval = fmt.Errorf("file data source monitoring interval must be >= %s", minimumFileDataSourceMonitoringInterval)
errInvalidCredentialCleanupInterval = fmt.Errorf("expired credential cleanup interval must be >= %s", minimumCredentialCleanupInterval)
)

func errEnvironmentWithNoSDKKey(envName string) error {
Expand Down Expand Up @@ -78,6 +79,7 @@ func ValidateConfig(c *Config, loggers ldlog.Loggers) error {
validateConfigDatabases(&result, c, loggers)
validateConfigFilters(&result, c)
validateOfflineMode(&result, c)
validateCredentialCleanupInterval(&result, c)

return result.GetError()
}
Expand Down Expand Up @@ -196,6 +198,14 @@ func validateOfflineMode(result *ct.ValidationResult, c *Config) {
}
}

func validateCredentialCleanupInterval(result *ct.ValidationResult, c *Config) {
if c.Main.ExpiredCredentialCleanupInterval.IsDefined() {
interval := c.Main.ExpiredCredentialCleanupInterval.GetOrElse(0)
if interval < minimumCredentialCleanupInterval {
result.AddError(nil, errInvalidCredentialCleanupInterval)
}
}
}
func validateConfigDatabases(result *ct.ValidationResult, c *Config, loggers ldlog.Loggers) {
normalizeRedisConfig(result, c)

Expand Down
13 changes: 13 additions & 0 deletions config/test_data_configs_invalid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ type testDataInvalidConfig struct {
func makeInvalidConfigs() []testDataInvalidConfig {
return []testDataInvalidConfig{
makeInvalidConfigMissingSDKKey(),
makeInvalidConfigCredentialCleanupInterval("0s"),
makeInvalidConfigCredentialCleanupInterval("-1s"),
makeInvalidConfigCredentialCleanupInterval("99ms"),
makeInvalidConfigTLSWithNoCertOrKey(),
makeInvalidConfigTLSWithNoCert(),
makeInvalidConfigTLSWithNoKey(),
Expand Down Expand Up @@ -255,6 +258,16 @@ fileDataSourceMonitoringInterval = ` + interval + `
return c
}

func makeInvalidConfigCredentialCleanupInterval(interval string) testDataInvalidConfig {
c := testDataInvalidConfig{name: "credential cleanup interval with invalid value"}
c.fileError = errInvalidCredentialCleanupInterval.Error()
c.fileContent = `
[Main]
expiredCredentialCleanupInterval = ` + interval + `
`
return c
}

func makeInvalidConfigRedisInvalidHostname() testDataInvalidConfig {
c := testDataInvalidConfig{name: "Redis - invalid hostname"}
c.envVarsError = "invalid Redis hostname"
Expand Down
111 changes: 57 additions & 54 deletions config/test_data_configs_valid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,24 @@ func makeValidConfigAllBaseProperties() testDataValidConfig {
c := testDataValidConfig{name: "all base properties"}
c.makeConfig = func(c *Config) {
c.Main = MainConfig{
Port: mustOptIntGreaterThanZero(8333),
BaseURI: newOptURLAbsoluteMustBeValid("http://base"),
ClientSideBaseURI: newOptURLAbsoluteMustBeValid("http://clientbase"),
StreamURI: newOptURLAbsoluteMustBeValid("http://stream"),
ExitOnError: true,
ExitAlways: true,
IgnoreConnectionErrors: true,
HeartbeatInterval: ct.NewOptDuration(90 * time.Second),
MaxClientConnectionTime: ct.NewOptDuration(30 * time.Minute),
DisconnectedStatusTime: ct.NewOptDuration(3 * time.Minute),
TLSEnabled: true,
TLSCert: "cert",
TLSKey: "key",
TLSMinVersion: NewOptTLSVersion(tls.VersionTLS12),
LogLevel: NewOptLogLevel(ldlog.Warn),
BigSegmentsStaleAsDegraded: true,
BigSegmentsStaleThreshold: ct.NewOptDuration(10 * time.Minute),
Port: mustOptIntGreaterThanZero(8333),
BaseURI: newOptURLAbsoluteMustBeValid("http://base"),
ClientSideBaseURI: newOptURLAbsoluteMustBeValid("http://clientbase"),
StreamURI: newOptURLAbsoluteMustBeValid("http://stream"),
ExitOnError: true,
ExitAlways: true,
IgnoreConnectionErrors: true,
HeartbeatInterval: ct.NewOptDuration(90 * time.Second),
MaxClientConnectionTime: ct.NewOptDuration(30 * time.Minute),
DisconnectedStatusTime: ct.NewOptDuration(3 * time.Minute),
TLSEnabled: true,
TLSCert: "cert",
TLSKey: "key",
TLSMinVersion: NewOptTLSVersion(tls.VersionTLS12),
LogLevel: NewOptLogLevel(ldlog.Warn),
BigSegmentsStaleAsDegraded: true,
BigSegmentsStaleThreshold: ct.NewOptDuration(10 * time.Minute),
ExpiredCredentialCleanupInterval: ct.NewOptDuration(1 * time.Minute),
}
c.Events = EventsConfig{
SendEvents: true,
Expand Down Expand Up @@ -146,43 +147,44 @@ func makeValidConfigAllBaseProperties() testDataValidConfig {
}
}
c.envVars = map[string]string{
"PORT": "8333",
"BASE_URI": "http://base",
"CLIENT_SIDE_BASE_URI": "http://clientbase",
"STREAM_URI": "http://stream",
"EXIT_ON_ERROR": "1",
"EXIT_ALWAYS": "1",
"IGNORE_CONNECTION_ERRORS": "1",
"HEARTBEAT_INTERVAL": "90s",
"MAX_CLIENT_CONNECTION_TIME": "30m",
"DISCONNECTED_STATUS_TIME": "3m",
"TLS_ENABLED": "1",
"TLS_CERT": "cert",
"TLS_KEY": "key",
"TLS_MIN_VERSION": "1.2",
"LOG_LEVEL": "warn",
"BIG_SEGMENTS_STALE_AS_DEGRADED": "true",
"BIG_SEGMENTS_STALE_THRESHOLD": "10m",
"USE_EVENTS": "1",
"EVENTS_HOST": "http://events",
"EVENTS_FLUSH_INTERVAL": "120s",
"EVENTS_CAPACITY": "500",
"EVENTS_INLINE_USERS": "1",
"LD_ENV_earth": "earth-sdk",
"LD_MOBILE_KEY_earth": "earth-mob",
"LD_CLIENT_SIDE_ID_earth": "earth-env",
"LD_PREFIX_earth": "earth-",
"LD_TABLE_NAME_earth": "earth-table",
"LD_LOG_LEVEL_earth": "debug",
"LD_ENV_krypton": "krypton-sdk",
"LD_MOBILE_KEY_krypton": "krypton-mob",
"LD_CLIENT_SIDE_ID_krypton": "krypton-env",
"LD_SECURE_MODE_krypton": "1",
"LD_PREFIX_krypton": "krypton-",
"LD_TABLE_NAME_krypton": "krypton-table",
"LD_ALLOWED_ORIGIN_krypton": "https://oa,https://rann",
"LD_ALLOWED_HEADER_krypton": "Timestamp-Valid,Random-Id-Valid",
"LD_TTL_krypton": "5m",
"PORT": "8333",
"BASE_URI": "http://base",
"CLIENT_SIDE_BASE_URI": "http://clientbase",
"STREAM_URI": "http://stream",
"EXIT_ON_ERROR": "1",
"EXIT_ALWAYS": "1",
"IGNORE_CONNECTION_ERRORS": "1",
"HEARTBEAT_INTERVAL": "90s",
"MAX_CLIENT_CONNECTION_TIME": "30m",
"DISCONNECTED_STATUS_TIME": "3m",
"TLS_ENABLED": "1",
"TLS_CERT": "cert",
"TLS_KEY": "key",
"TLS_MIN_VERSION": "1.2",
"LOG_LEVEL": "warn",
"BIG_SEGMENTS_STALE_AS_DEGRADED": "true",
"BIG_SEGMENTS_STALE_THRESHOLD": "10m",
"USE_EVENTS": "1",
"EVENTS_HOST": "http://events",
"EVENTS_FLUSH_INTERVAL": "120s",
"EVENTS_CAPACITY": "500",
"EVENTS_INLINE_USERS": "1",
"LD_ENV_earth": "earth-sdk",
"LD_MOBILE_KEY_earth": "earth-mob",
"LD_CLIENT_SIDE_ID_earth": "earth-env",
"LD_PREFIX_earth": "earth-",
"LD_TABLE_NAME_earth": "earth-table",
"LD_LOG_LEVEL_earth": "debug",
"LD_ENV_krypton": "krypton-sdk",
"LD_MOBILE_KEY_krypton": "krypton-mob",
"LD_CLIENT_SIDE_ID_krypton": "krypton-env",
"LD_SECURE_MODE_krypton": "1",
"LD_PREFIX_krypton": "krypton-",
"LD_TABLE_NAME_krypton": "krypton-table",
"LD_ALLOWED_ORIGIN_krypton": "https://oa,https://rann",
"LD_ALLOWED_HEADER_krypton": "Timestamp-Valid,Random-Id-Valid",
"LD_TTL_krypton": "5m",
"EXPIRED_CREDENTIAL_CLEANUP_INTERVAL": "1m",
}
c.fileContent = `
[Main]
Expand All @@ -203,6 +205,7 @@ TLSMinVersion = "1.2"
LogLevel = "warn"
BigSegmentsStaleAsDegraded = 1
BigSegmentsStaleThreshold = 10m
ExpiredCredentialCleanupInterval = 1m

[Events]
SendEvents = 1
Expand Down
Loading
Loading