fix(configuration): secret permission errors panic (#5141)

This fixes an issue where attempting to load secrets the process does not  have read permissions for would cause panics as well as the bit size check of the OpenID Connect 1.0 private key can potentially panic on malformed private keys. This was caused by us returning values on errors instead of nil's.

Fixes #5138

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/5191/head
James Elliott 2023-04-08 16:02:34 +10:00 committed by GitHub
parent 0424652940
commit 622bf42ed4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 113 additions and 52 deletions

View File

@ -28,7 +28,9 @@ var (
const ( const (
errFmtSecretAlreadyDefined = "secrets: error loading secret into key '%s': it's already defined in other " + errFmtSecretAlreadyDefined = "secrets: error loading secret into key '%s': it's already defined in other " +
"configuration sources" "configuration sources"
errFmtSecretIOIssue = "secrets: error loading secret path %s into key '%s': %v" errFmtSecretOSError = "secrets: error loading secret path %s into key '%s': %w"
errFmtSecretOSPermission = "secrets: error loading secret path %s into key '%s': file permission error occurred: %w"
errFmtSecretOSNotExist = "secrets: error loading secret path %s into key '%s': file does not exist error occurred: %w"
errFmtGenerateConfiguration = "error occurred generating configuration: %+v" errFmtGenerateConfiguration = "error occurred generating configuration: %+v"
errFmtDecodeHookCouldNotParse = "could not decode '%s' to a %s%s: %w" errFmtDecodeHookCouldNotParse = "could not decode '%s' to a %s%s: %w"

View File

@ -2,6 +2,7 @@ package configuration
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -40,13 +41,25 @@ func koanfEnvironmentSecretsCallback(keyMap map[string]string, validator *schema
return "", nil return "", nil
} }
v, err := loadSecret(value) switch v, err := loadSecret(value); err {
if err != nil { case nil:
validator.Push(fmt.Errorf(errFmtSecretIOIssue, value, k, err))
return k, ""
}
return k, v return k, v
default:
switch {
case os.IsNotExist(err):
validator.Push(fmt.Errorf(errFmtSecretOSNotExist, value, k, err))
return "", nil
case os.IsPermission(err):
validator.Push(fmt.Errorf(errFmtSecretOSPermission, value, k, err))
return "", nil
default:
validator.Push(fmt.Errorf(errFmtSecretOSError, value, k, err))
return "", nil
}
}
} }
} }

View File

@ -117,10 +117,10 @@ func TestKoanfSecretCallbackShouldErrorOnFSError(t *testing.T) {
callback := koanfEnvironmentSecretsCallback(keyMap, val) callback := koanfEnvironmentSecretsCallback(keyMap, val)
key, value := callback("AUTHELIA_THEME", secret) key, value := callback("AUTHELIA_THEME", secret)
assert.Equal(t, "theme", key) assert.Equal(t, "", key)
assert.Equal(t, "", value) assert.Equal(t, nil, value)
require.Len(t, val.Errors(), 1) require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0) assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], fmt.Sprintf(errFmtSecretIOIssue, secret, "theme", fmt.Sprintf("open %s: permission denied", secret))) assert.EqualError(t, val.Errors()[0], fmt.Sprintf("secrets: error loading secret path %s into key 'theme': file permission error occurred: open %s: permission denied", secret, secret))
} }

View File

@ -22,7 +22,7 @@ func TestShouldErrorSecretNotExist(t *testing.T) {
testSetEnv(t, "JWT_SECRET_FILE", filepath.Join(dir, "jwt")) testSetEnv(t, "JWT_SECRET_FILE", filepath.Join(dir, "jwt"))
testSetEnv(t, "DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo")) testSetEnv(t, "DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo"))
testSetEnv(t, "SESSION_SECRET_FILE", filepath.Join(dir, "session")) testSetEnv(t, "SESSION_SECRET_FILE", filepath.Join(dir, "session"))
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", filepath.Join(dir, "authentication")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir)
testSetEnv(t, "NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier")) testSetEnv(t, "NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier"))
testSetEnv(t, "SESSION_REDIS_PASSWORD_FILE", filepath.Join(dir, "redis")) testSetEnv(t, "SESSION_REDIS_PASSWORD_FILE", filepath.Join(dir, "redis"))
testSetEnv(t, "SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", filepath.Join(dir, "redis-sentinel")) testSetEnv(t, "SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", filepath.Join(dir, "redis-sentinel"))
@ -44,20 +44,21 @@ func TestShouldErrorSecretNotExist(t *testing.T) {
sort.Sort(utils.ErrSliceSortAlphabetical(errs)) sort.Sort(utils.ErrSliceSortAlphabetical(errs))
errFmt := utils.GetExpectedErrTxt("filenotfound") errFmt := utils.GetExpectedErrTxt("filenotfound")
errFmtDir := utils.GetExpectedErrTxt("isdir")
// ignore the errors before this as they are checked by the validator. // ignore the errors before this as they are checked by the validator.
assert.EqualError(t, errs[0], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "authentication"), "authentication_backend.ldap.password", fmt.Sprintf(errFmt, filepath.Join(dir, "authentication")))) assert.EqualError(t, errs[0], fmt.Sprintf("secrets: error loading secret path %s into key 'authentication_backend.ldap.password': %s", dir, fmt.Sprintf(errFmtDir, dir)))
assert.EqualError(t, errs[1], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "duo"), "duo_api.secret_key", fmt.Sprintf(errFmt, filepath.Join(dir, "duo")))) assert.EqualError(t, errs[1], fmt.Sprintf("secrets: error loading secret path %s into key 'duo_api.secret_key': file does not exist error occurred: %s", filepath.Join(dir, "duo"), fmt.Sprintf(errFmt, filepath.Join(dir, "duo"))))
assert.EqualError(t, errs[2], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "jwt"), "jwt_secret", fmt.Sprintf(errFmt, filepath.Join(dir, "jwt")))) assert.EqualError(t, errs[2], fmt.Sprintf("secrets: error loading secret path %s into key 'jwt_secret': file does not exist error occurred: %s", filepath.Join(dir, "jwt"), fmt.Sprintf(errFmt, filepath.Join(dir, "jwt"))))
assert.EqualError(t, errs[3], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "mysql"), "storage.mysql.password", fmt.Sprintf(errFmt, filepath.Join(dir, "mysql")))) assert.EqualError(t, errs[3], fmt.Sprintf("secrets: error loading secret path %s into key 'storage.mysql.password': file does not exist error occurred: %s", filepath.Join(dir, "mysql"), fmt.Sprintf(errFmt, filepath.Join(dir, "mysql"))))
assert.EqualError(t, errs[4], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "notifier"), "notifier.smtp.password", fmt.Sprintf(errFmt, filepath.Join(dir, "notifier")))) assert.EqualError(t, errs[4], fmt.Sprintf("secrets: error loading secret path %s into key 'notifier.smtp.password': file does not exist error occurred: %s", filepath.Join(dir, "notifier"), fmt.Sprintf(errFmt, filepath.Join(dir, "notifier"))))
assert.EqualError(t, errs[5], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "oidc-hmac"), "identity_providers.oidc.hmac_secret", fmt.Sprintf(errFmt, filepath.Join(dir, "oidc-hmac")))) assert.EqualError(t, errs[5], fmt.Sprintf("secrets: error loading secret path %s into key 'identity_providers.oidc.hmac_secret': file does not exist error occurred: %s", filepath.Join(dir, "oidc-hmac"), fmt.Sprintf(errFmt, filepath.Join(dir, "oidc-hmac"))))
assert.EqualError(t, errs[6], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "oidc-key"), "identity_providers.oidc.issuer_private_key", fmt.Sprintf(errFmt, filepath.Join(dir, "oidc-key")))) assert.EqualError(t, errs[6], fmt.Sprintf("secrets: error loading secret path %s into key 'identity_providers.oidc.issuer_private_key': file does not exist error occurred: %s", filepath.Join(dir, "oidc-key"), fmt.Sprintf(errFmt, filepath.Join(dir, "oidc-key"))))
assert.EqualError(t, errs[7], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "postgres"), "storage.postgres.password", fmt.Sprintf(errFmt, filepath.Join(dir, "postgres")))) assert.EqualError(t, errs[7], fmt.Sprintf("secrets: error loading secret path %s into key 'storage.postgres.password': file does not exist error occurred: %s", filepath.Join(dir, "postgres"), fmt.Sprintf(errFmt, filepath.Join(dir, "postgres"))))
assert.EqualError(t, errs[8], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "redis"), "session.redis.password", fmt.Sprintf(errFmt, filepath.Join(dir, "redis")))) assert.EqualError(t, errs[8], fmt.Sprintf("secrets: error loading secret path %s into key 'session.redis.password': file does not exist error occurred: %s", filepath.Join(dir, "redis"), fmt.Sprintf(errFmt, filepath.Join(dir, "redis"))))
assert.EqualError(t, errs[9], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "redis-sentinel"), "session.redis.high_availability.sentinel_password", fmt.Sprintf(errFmt, filepath.Join(dir, "redis-sentinel")))) assert.EqualError(t, errs[9], fmt.Sprintf("secrets: error loading secret path %s into key 'session.redis.high_availability.sentinel_password': file does not exist error occurred: %s", filepath.Join(dir, "redis-sentinel"), fmt.Sprintf(errFmt, filepath.Join(dir, "redis-sentinel"))))
assert.EqualError(t, errs[10], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "session"), "session.secret", fmt.Sprintf(errFmt, filepath.Join(dir, "session")))) assert.EqualError(t, errs[10], fmt.Sprintf("secrets: error loading secret path %s into key 'session.secret': file does not exist error occurred: %s", filepath.Join(dir, "session"), fmt.Sprintf(errFmt, filepath.Join(dir, "session"))))
assert.EqualError(t, errs[11], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "tls"), "server.tls.key", fmt.Sprintf(errFmt, filepath.Join(dir, "tls")))) assert.EqualError(t, errs[11], fmt.Sprintf("secrets: error loading secret path %s into key 'server.tls.key': file does not exist error occurred: %s", filepath.Join(dir, "tls"), fmt.Sprintf(errFmt, filepath.Join(dir, "tls"))))
} }
func TestLoadShouldReturnErrWithoutValidator(t *testing.T) { func TestLoadShouldReturnErrWithoutValidator(t *testing.T) {

View File

@ -144,6 +144,7 @@ const (
"more clients configured" "more clients configured"
errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' is required" errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' is required"
errFmtOIDCInvalidPrivateKeyBitSize = "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with %d bits or more but it only has %d bits" errFmtOIDCInvalidPrivateKeyBitSize = "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with %d bits or more but it only has %d bits"
errFmtOIDCInvalidPrivateKeyMalformedMissingPublicKey = "identity_providers: oidc: option 'issuer_private_key' must be a valid RSA private key but the provided data is missing the public key bits"
errFmtOIDCCertificateMismatch = "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'" errFmtOIDCCertificateMismatch = "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'"
errFmtOIDCCertificateChain = "identity_providers: oidc: option 'issuer_certificate_chain' produced an error during validation of the chain: %w" errFmtOIDCCertificateChain = "identity_providers: oidc: option 'issuer_certificate_chain' produced an error during validation of the chain: %w"
errFmtOIDCEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " + errFmtOIDCEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " +

View File

@ -37,7 +37,9 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructV
} }
} }
if config.IssuerPrivateKey.Size()*8 < 2048 { if config.IssuerPrivateKey.PublicKey.N == nil {
val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyMalformedMissingPublicKey))
} else if config.IssuerPrivateKey.Size()*8 < 2048 {
val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyBitSize, 2048, config.IssuerPrivateKey.Size()*8)) val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyBitSize, 2048, config.IssuerPrivateKey.Size()*8))
} }
} }

View File

@ -525,6 +525,35 @@ func TestShouldRaiseErrorOnKeySizeTooSmall(t *testing.T) {
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with 2048 bits or more but it only has 1024 bits") assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with 2048 bits or more but it only has 1024 bits")
} }
func TestShouldRaiseErrorOnKeyInvalidPublicKey(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: MustParseRSAPrivateKey(testKey3),
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "good_id",
Secret: MustDecodeSecret(goodOpenIDConnectClientSecret),
Policy: "two_factor",
RedirectURIs: []string{
"https://google.com/callback",
},
},
},
},
}
config.OIDC.IssuerPrivateKey.PublicKey.N = nil
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' must be a valid RSA private key but the provided data is missing the public key bits")
}
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{ config := &schema.IdentityProvidersConfiguration{

View File

@ -123,5 +123,14 @@ var ErrTimeoutReached = errors.New("timeout reached")
const ( const (
windows = "windows" windows = "windows"
errFmtLinuxNotFound = "open %s: no such file or directory" errFmtLinuxNotFound = "%s %%s: no such file or directory"
errFmtWindowsNotFound = "%s %%s: The system cannot find the %s specified."
strStat = "stat"
strOpen = "open"
strFile = "file"
strPath = "path"
strIsDir = "isdir"
strPathNotFound = "pathnotfound"
strFileNotFound = "filenotfound"
) )

View File

@ -1,6 +1,9 @@
package utils package utils
import "runtime" import (
"fmt"
"runtime"
)
// ErrSliceSortAlphabetical is a helper type that can be used with sort.Sort to sort a slice of errors in alphabetical // ErrSliceSortAlphabetical is a helper type that can be used with sort.Sort to sort a slice of errors in alphabetical
// order. Usage is simple just do sort.Sort(ErrSliceSortAlphabetical([]error{})). // order. Usage is simple just do sort.Sort(ErrSliceSortAlphabetical([]error{})).
@ -12,28 +15,29 @@ func (s ErrSliceSortAlphabetical) Less(i, j int) bool { return s[i].Error() < s[
func (s ErrSliceSortAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ErrSliceSortAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// GetExpectedErrTxt returns error text for expected errs. // GetExpectedErrTxt returns error text for expected errs. THIS IS A TEST UTILITY FUNCTION.
func GetExpectedErrTxt(err string) string { func GetExpectedErrTxt(err string) string {
switch runtime.GOOS {
case windows:
switch err { switch err {
case "pathnotfound": case strPathNotFound:
switch runtime.GOOS { return fmt.Sprintf(errFmtWindowsNotFound, strOpen, strPath)
case windows: case strStat + strPathNotFound:
return "open %s: The system cannot find the path specified." return fmt.Sprintf(errFmtWindowsNotFound, strStat, strPath)
default: case strFileNotFound:
return errFmtLinuxNotFound return fmt.Sprintf(errFmtWindowsNotFound, strOpen, strFile)
} case strStat + strFileNotFound:
case "filenotfound": return fmt.Sprintf(errFmtWindowsNotFound, strStat, strFile)
switch runtime.GOOS { case strIsDir:
case windows:
return "open %s: The system cannot find the file specified."
default:
return errFmtLinuxNotFound
}
case "yamlisdir":
switch runtime.GOOS {
case windows:
return "read %s: The handle is invalid." return "read %s: The handle is invalid."
}
default: default:
switch err {
case strPathNotFound, strFileNotFound:
return fmt.Sprintf(errFmtLinuxNotFound, strOpen)
case strStat + strPathNotFound, strStat + strFileNotFound:
return fmt.Sprintf(errFmtLinuxNotFound, strStat)
case strIsDir:
return "read %s: is a directory" return "read %s: is a directory"
} }
} }