diff --git a/docs/configuration/secrets.md b/docs/configuration/secrets.md index e9e565582..11c23b1bf 100644 --- a/docs/configuration/secrets.md +++ b/docs/configuration/secrets.md @@ -31,16 +31,17 @@ Here is the list of the environment variables which are considered secrets and can be defined. Any other option defined using an environment variable will not be replaced. -|Configuration Key |Environment Variable | -|:----------------------------------:|:------------------------------------------------:| -|jwt_secret |AUTHELIA_JWT_SECRET_FILE | -|duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE | -|session.secret |AUTHELIA_SESSION_SECRET_FILE | -|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE | -|storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE | -|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE | -|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE | -|authentication_backend.ldap.password|AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE| +|Configuration Key |Environment Variable | +|:-----------------------------------------------:|:------------------------------------------------:| +|jwt_secret |AUTHELIA_JWT_SECRET_FILE | +|duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE | +|session.secret |AUTHELIA_SESSION_SECRET_FILE | +|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE | +|session.redis.high_availability.sentinel_password|AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD| +|storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE | +|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE | +|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE | +|authentication_backend.ldap.password |AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE| ## Secrets in configuration file diff --git a/internal/authentication/file_user_provider_test.go b/internal/authentication/file_user_provider_test.go index dba480951..ffdb0e86a 100644 --- a/internal/authentication/file_user_provider_test.go +++ b/internal/authentication/file_user_provider_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "log" "os" + "runtime" "strings" "testing" @@ -34,6 +35,10 @@ func WithDatabase(content []byte, f func(path string)) { } func TestShouldErrorPermissionsOnLocalFS(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test due to being on windows") + } + _ = os.Mkdir("/tmp/noperms/", 0000) errors := checkDatabase("/tmp/noperms/users_database.yml") diff --git a/internal/configuration/const.go b/internal/configuration/const.go new file mode 100644 index 000000000..961c210be --- /dev/null +++ b/internal/configuration/const.go @@ -0,0 +1,3 @@ +package configuration + +const windows = "windows" diff --git a/internal/configuration/reader.go b/internal/configuration/reader.go index 588d7fd54..12a3ff1e1 100644 --- a/internal/configuration/reader.go +++ b/internal/configuration/reader.go @@ -55,14 +55,10 @@ func Read(configPath string) (*schema.Configuration, []error) { viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.BindEnv("authelia.jwt_secret.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.duo_api.secret_key.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.session.secret.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.authentication_backend.ldap.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.notifier.smtp.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.session.redis.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.storage.mysql.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authelia.storage.postgres.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + // Dynamically load the secret env names from the SecretNames map. + for _, secretName := range validator.SecretNames { + _ = viper.BindEnv(validator.SecretNameToEnvName(secretName)) + } viper.SetConfigFile(configPath) diff --git a/internal/configuration/reader_test.go b/internal/configuration/reader_test.go index 8e57bfdc1..95d37acf5 100644 --- a/internal/configuration/reader_test.go +++ b/internal/configuration/reader_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path" + "runtime" "sort" "testing" @@ -27,6 +28,7 @@ func resetEnv() { _ = os.Unsetenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE") + _ = os.Unsetenv("AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE") } @@ -49,6 +51,7 @@ func setupEnv(t *testing.T) string { createTestingTempFile(t, dir, "authentication", "ldap_secret_from_env") createTestingTempFile(t, dir, "notifier", "smtp_secret_from_env") createTestingTempFile(t, dir, "redis", "redis_secret_from_env") + createTestingTempFile(t, dir, "redis-sentinel", "redis-sentinel_secret_from_env") createTestingTempFile(t, dir, "mysql", "mysql_secret_from_env") createTestingTempFile(t, dir, "postgres", "postgres_secret_from_env") @@ -65,7 +68,56 @@ func TestShouldErrorNoConfigPath(t *testing.T) { require.EqualError(t, errors[0], "No config file path provided") } +func TestShouldErrorSecretNotExist(t *testing.T) { + dir := "/path/not/exist" + + require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET_FILE", dir+"jwt")) + require.NoError(t, os.Setenv("AUTHELIA_DUO_API_SECRET_KEY_FILE", dir+"duo")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_SECRET_FILE", dir+"session")) + require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication")) + require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE", dir+"notifier")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE", dir+"redis")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", dir+"redis-sentinel")) + require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql")) + require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres")) + + _, errors := Read("./test_resources/config.yml") + + require.Len(t, errors, 12) + + if runtime.GOOS == windows { + assert.EqualError(t, errors[0], "error loading secret file (jwt_secret): open /path/not/existjwt: The system cannot find the path specified.") + assert.EqualError(t, errors[1], "error loading secret file (session.secret): open /path/not/existsession: The system cannot find the path specified.") + assert.EqualError(t, errors[2], "error loading secret file (duo_api.secret_key): open /path/not/existduo: The system cannot find the path specified.") + assert.EqualError(t, errors[3], "error loading secret file (session.redis.password): open /path/not/existredis: The system cannot find the path specified.") + assert.EqualError(t, errors[4], "error loading secret file (session.redis.high_availability.sentinel_password): open /path/not/existredis-sentinel: The system cannot find the path specified.") + assert.EqualError(t, errors[5], "error loading secret file (authentication_backend.ldap.password): open /path/not/existauthentication: The system cannot find the path specified.") + assert.EqualError(t, errors[6], "error loading secret file (notifier.smtp.password): open /path/not/existnotifier: The system cannot find the path specified.") + assert.EqualError(t, errors[7], "error loading secret file (storage.mysql.password): open /path/not/existmysql: The system cannot find the path specified.") + } else { + assert.EqualError(t, errors[0], "error loading secret file (jwt_secret): open /path/not/existjwt: no such file or directory") + assert.EqualError(t, errors[1], "error loading secret file (session.secret): open /path/not/existsession: no such file or directory") + assert.EqualError(t, errors[2], "error loading secret file (duo_api.secret_key): open /path/not/existduo: no such file or directory") + assert.EqualError(t, errors[3], "error loading secret file (session.redis.password): open /path/not/existredis: no such file or directory") + assert.EqualError(t, errors[4], "error loading secret file (session.redis.high_availability.sentinel_password): open /path/not/existredis-sentinel: no such file or directory") + assert.EqualError(t, errors[5], "error loading secret file (authentication_backend.ldap.password): open /path/not/existauthentication: no such file or directory") + assert.EqualError(t, errors[6], "error loading secret file (notifier.smtp.password): open /path/not/existnotifier: no such file or directory") + assert.EqualError(t, errors[7], "error loading secret file (storage.mysql.password): open /path/not/existmysql: no such file or directory") + } + + assert.EqualError(t, errors[8], "Provide a JWT secret using \"jwt_secret\" key") + assert.EqualError(t, errors[9], "Please provide a password to connect to the LDAP server") + assert.EqualError(t, errors[10], "The session secret must be set when using the redis sentinel session provider") + assert.EqualError(t, errors[11], "the SQL username and password must be provided") +} + func TestShouldErrorPermissionsOnLocalFS(t *testing.T) { + if runtime.GOOS == windows { + t.Skip("skipping test due to being on windows") + } + + resetEnv() + _ = os.Mkdir("/tmp/noperms/", 0000) _, errors := Read("/tmp/noperms/configuration.yml") @@ -88,12 +140,23 @@ func TestShouldErrorAndGenerateConfigFile(t *testing.T) { } func TestShouldErrorPermissionsConfigFile(t *testing.T) { + resetEnv() + _ = ioutil.WriteFile("/tmp/authelia/permissions.yml", []byte{}, 0000) // nolint:gosec _, errors := Read("/tmp/authelia/permissions.yml") - require.Len(t, errors, 1) + if runtime.GOOS == windows { + require.Len(t, errors, 5) + assert.EqualError(t, errors[0], "Provide a JWT secret using \"jwt_secret\" key") + assert.EqualError(t, errors[1], "Please provide `ldap` or `file` object in `authentication_backend`") + assert.EqualError(t, errors[2], "Set domain of the session object") + assert.EqualError(t, errors[3], "A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres'") + assert.EqualError(t, errors[4], "A notifier configuration must be provided") + } else { + require.Len(t, errors, 1) - require.EqualError(t, errors[0], "Failed to open /tmp/authelia/permissions.yml: permission denied") + assert.EqualError(t, errors[0], "Failed to open /tmp/authelia/permissions.yml: permission denied") + } } func TestShouldErrorParseBadConfigFile(t *testing.T) { @@ -113,6 +176,7 @@ func TestShouldParseConfigFile(t *testing.T) { require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication")) require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE", dir+"notifier")) require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE", dir+"redis")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", dir+"redis-sentinel")) require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql")) require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres")) @@ -134,10 +198,15 @@ func TestShouldParseConfigFile(t *testing.T) { assert.Equal(t, "ldap_secret_from_env", config.AuthenticationBackend.Ldap.Password) assert.Equal(t, "smtp_secret_from_env", config.Notifier.SMTP.Password) assert.Equal(t, "redis_secret_from_env", config.Session.Redis.Password) + assert.Equal(t, "redis-sentinel_secret_from_env", config.Session.Redis.HighAvailability.SentinelPassword) assert.Equal(t, "mysql_secret_from_env", config.Storage.MySQL.Password) assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) assert.Len(t, config.AccessControl.Rules, 12) + + require.NotNil(t, config.Session) + require.NotNil(t, config.Session.Redis) + require.NotNil(t, config.Session.Redis.HighAvailability) } func TestShouldParseAltConfigFile(t *testing.T) { diff --git a/internal/configuration/test_resources/config.yml b/internal/configuration/test_resources/config.yml index f34bf8959..110c79958 100644 --- a/internal/configuration/test_resources/config.yml +++ b/internal/configuration/test_resources/config.yml @@ -101,6 +101,8 @@ session: redis: host: 127.0.0.1 port: 6379 + high_availability: + sentinel_name: test regulation: max_retries: 3 diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index a0a6115e5..a024592cc 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -92,10 +92,6 @@ func validateLdapURL(ldapURL string, validator *schema.StructValidator) (finalUR return "", "" } - if !parsedURL.IsAbs() { - validator.Push(fmt.Errorf("URL to LDAP %s is still not absolute, it should be something like ldap://127.0.0.1:389", parsedURL.String())) - } - return parsedURL.String(), parsedURL.Hostname() } diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index 574a5b041..873ecdea1 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -10,7 +10,22 @@ import ( "github.com/authelia/authelia/internal/configuration/schema" ) -func TestShouldRaiseErrorsWhenNoBackendProvided(t *testing.T) { +func TestShouldRaiseErrorWhenBothBackendsProvided(t *testing.T) { + validator := schema.NewStructValidator() + backendConfig := schema.AuthenticationBackendConfiguration{} + + backendConfig.Ldap = &schema.LDAPAuthenticationBackendConfiguration{} + backendConfig.File = &schema.FileAuthenticationBackendConfiguration{ + Path: "/tmp", + } + + ValidateAuthenticationBackend(&backendConfig, validator) + + require.Len(t, validator.Errors(), 1) + assert.EqualError(t, validator.Errors()[0], "You cannot provide both `ldap` and `file` objects in `authentication_backend`") +} + +func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) { validator := schema.NewStructValidator() backendConfig := schema.AuthenticationBackendConfiguration{} diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go index ea60e42df..e589c2177 100644 --- a/internal/configuration/validator/configuration.go +++ b/internal/configuration/validator/configuration.go @@ -8,7 +8,7 @@ import ( "github.com/authelia/authelia/internal/configuration/schema" ) -var defaultPort = 8080 +var defaultPort = 9091 var defaultLogLevel = "info" // ValidateConfiguration and adapt the configuration read from file. diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index dd036f8d3..3024da0f6 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -1,6 +1,7 @@ package validator import ( + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -54,7 +55,7 @@ func TestShouldValidateAndUpdatePort(t *testing.T) { ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) - assert.Equal(t, 8080, config.Port) + assert.Equal(t, 9091, config.Port) } func TestShouldValidateAndUpdateHost(t *testing.T) { @@ -170,7 +171,12 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) { ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: stat not-a-real-file.go: no such file or directory") + + if runtime.GOOS == "windows" { + assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: CreateFile not-a-real-file.go: The system cannot find the file specified.") + } else { + assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: stat not-a-real-file.go: no such file or directory") + } validator = schema.NewStructValidator() config.CertificatesDirectory = "const.go" diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index fbd0af721..c06484c8d 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -1,7 +1,57 @@ package validator +const ( + errFmtSessionSecretRedisProvider = "The session secret must be set when using the %s session provider" + errFmtSessionRedisPortRange = "The port must be between 1 and 65535 for the %s session provider" + errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider" + errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider" + + errFileHashing = "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password" + errFilePHashing = "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password" + errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password" + + denyPolicy = "deny" + bypassPolicy = "bypass" + + argon2id = "argon2id" + sha512 = "sha512" + + schemeLDAP = "ldap" + schemeLDAPS = "ldaps" + + testBadTimer = "-1" + testInvalidPolicy = "invalid" + testJWTSecret = "a_secret" + testLDAPBaseDN = "base_dn" + testLDAPPassword = "password" + testLDAPURL = "ldap://ldap" + testLDAPUser = "user" + testModeDisabled = "disable" + testTLSCert = "/tmp/cert.pem" + testTLSKey = "/tmp/key.pem" + + errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for domain %s with subjects %s is invalid. It is " + + "not supported to configure both policy bypass and subjects. For more information see: " + + "https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy" +) + var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} +// SecretNames contains a map of secret names. +var SecretNames = map[string]string{ + "JWTSecret": "jwt_secret", + "SessionSecret": "session.secret", + "DUOSecretKey": "duo_api.secret_key", + "RedisPassword": "session.redis.password", + "RedisSentinelPassword": "session.redis.high_availability.sentinel_password", + "LDAPPassword": "authentication_backend.ldap.password", + "SMTPPassword": "notifier.smtp.password", + "MySQLPassword": "storage.mysql.password", + "PostgreSQLPassword": "storage.postgres.password", +} + +// validKeys is a list of valid keys that are not secret names. For the sake of consistency please place any secret in +// the secret names map and reuse it in relevant sections. var validKeys = []string{ // Root Keys. "host", @@ -10,7 +60,6 @@ var validKeys = []string{ "log_format", "log_file_path", "default_redirection_url", - "jwt_secret", "theme", "tls_key", "tls_cert", @@ -33,7 +82,6 @@ var validKeys = []string{ // Session Keys. "session.name", - "session.secret", "session.expiration", "session.inactivity", "session.remember_me_duration", @@ -43,7 +91,6 @@ var validKeys = []string{ "session.redis.host", "session.redis.port", "session.redis.username", - "session.redis.password", "session.redis.database_index", "session.redis.maximum_active_connections", "session.redis.minimum_idle_connections", @@ -51,7 +98,6 @@ var validKeys = []string{ "session.redis.tls.skip_verify", "session.redis.tls.server_name", "session.redis.high_availability.sentinel_name", - "session.redis.high_availability.sentinel_password", "session.redis.high_availability.nodes", "session.redis.high_availability.route_by_latency", "session.redis.high_availability.route_randomly", @@ -69,14 +115,12 @@ var validKeys = []string{ "storage.mysql.port", "storage.mysql.database", "storage.mysql.username", - "storage.mysql.password", // PostgreSQL Storage Keys. "storage.postgres.host", "storage.postgres.port", "storage.postgres.database", "storage.postgres.username", - "storage.postgres.password", "storage.postgres.sslmode", // FileSystem Notifier Keys. @@ -85,7 +129,6 @@ var validKeys = []string{ // SMTP Notifier Keys. "notifier.smtp.username", - "notifier.smtp.password", "notifier.smtp.host", "notifier.smtp.port", "notifier.smtp.identifier", @@ -108,7 +151,6 @@ var validKeys = []string{ // DUO API Keys. "duo_api.hostname", "duo_api.integration_key", - "duo_api.secret_key", // Authentication Backend Keys. "authentication_backend.disable_reset_password", @@ -127,7 +169,6 @@ var validKeys = []string{ "authentication_backend.ldap.mail_attribute", "authentication_backend.ldap.display_name_attribute", "authentication_backend.ldap.user", - "authentication_backend.ldap.password", "authentication_backend.ldap.start_tls", "authentication_backend.ldap.tls.minimum_version", "authentication_backend.ldap.tls.skip_verify", @@ -143,73 +184,28 @@ var validKeys = []string{ "authentication_backend.file.password.salt_length", "authentication_backend.file.password.memory", "authentication_backend.file.password.parallelism", - - // Secret Keys. - "authelia.jwt_secret", - "authelia.duo_api.secret_key", - "authelia.session.secret", - "authelia.authentication_backend.ldap.password", - "authelia.notifier.smtp.password", - "authelia.session.redis.password", - "authelia.storage.mysql.password", - "authelia.storage.postgres.password", - "authelia.jwt_secret.file", - "authelia.duo_api.secret_key.file", - "authelia.session.secret.file", - "authelia.authentication_backend.ldap.password.file", - "authelia.notifier.smtp.password.file", - "authelia.session.redis.password.file", - "authelia.storage.mysql.password.file", - "authelia.storage.postgres.password.file", } var specificErrorKeys = map[string]string{ "logs_file_path": "config key replaced: logs_file is now log_file", "logs_level": "config key replaced: logs_level is now log_level", "google_analytics": "config key removed: google_analytics - this functionality has been deprecated", - "authentication_backend.file.password_options.algorithm": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", - "authentication_backend.file.password_options.iterations": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", - "authentication_backend.file.password_options.key_length": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", - "authentication_backend.file.password_options.salt_length": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", - "authentication_backend.file.password_options.memory": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", - "authentication_backend.file.password_options.parallelism": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", - "authentication_backend.file.password_hashing.algorithm": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", - "authentication_backend.file.password_hashing.iterations": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", - "authentication_backend.file.password_hashing.key_length": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", - "authentication_backend.file.password_hashing.salt_length": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", - "authentication_backend.file.password_hashing.memory": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", - "authentication_backend.file.password_hashing.parallelism": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", - "authentication_backend.file.hashing.algorithm": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", - "authentication_backend.file.hashing.iterations": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", - "authentication_backend.file.hashing.key_length": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", - "authentication_backend.file.hashing.salt_length": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", - "authentication_backend.file.hashing.memory": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", - "authentication_backend.file.hashing.parallelism": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", + "authentication_backend.file.password_options.algorithm": errFilePOptions, + "authentication_backend.file.password_options.iterations": errFilePOptions, + "authentication_backend.file.password_options.key_length": errFilePOptions, + "authentication_backend.file.password_options.salt_length": errFilePOptions, + "authentication_backend.file.password_options.memory": errFilePOptions, + "authentication_backend.file.password_options.parallelism": errFilePOptions, + "authentication_backend.file.password_hashing.algorithm": errFilePHashing, + "authentication_backend.file.password_hashing.iterations": errFilePHashing, + "authentication_backend.file.password_hashing.key_length": errFilePHashing, + "authentication_backend.file.password_hashing.salt_length": errFilePHashing, + "authentication_backend.file.password_hashing.memory": errFilePHashing, + "authentication_backend.file.password_hashing.parallelism": errFilePHashing, + "authentication_backend.file.hashing.algorithm": errFileHashing, + "authentication_backend.file.hashing.iterations": errFileHashing, + "authentication_backend.file.hashing.key_length": errFileHashing, + "authentication_backend.file.hashing.salt_length": errFileHashing, + "authentication_backend.file.hashing.memory": errFileHashing, + "authentication_backend.file.hashing.parallelism": errFileHashing, } - -const errFmtSessionSecretRedisProvider = "The session secret must be set when using the %s session provider" -const errFmtSessionRedisPortRange = "The port must be between 1 and 65535 for the %s session provider" -const errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider" -const errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider" - -const denyPolicy = "deny" -const bypassPolicy = "bypass" - -const argon2id = "argon2id" -const sha512 = "sha512" - -const schemeLDAP = "ldap" -const schemeLDAPS = "ldaps" - -const testBadTimer = "-1" -const testInvalidPolicy = "invalid" -const testJWTSecret = "a_secret" -const testLDAPBaseDN = "base_dn" -const testLDAPPassword = "password" -const testLDAPURL = "ldap://ldap" -const testLDAPUser = "user" -const testModeDisabled = "disable" -const testTLSCert = "/tmp/cert.pem" -const testTLSKey = "/tmp/key.pem" - -const errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for domain %s with subjects %s is invalid. It is not supported to configure both policy bypass and subjects. For more information see: https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy" diff --git a/internal/configuration/validator/keys.go b/internal/configuration/validator/keys.go index bc20effcf..6412312a0 100644 --- a/internal/configuration/validator/keys.go +++ b/internal/configuration/validator/keys.go @@ -17,6 +17,10 @@ func ValidateKeys(validator *schema.StructValidator, keys []string) { continue } + if isSecretKey(key) { + continue + } + if err, ok := specificErrorKeys[key]; ok { if !utils.IsStringInSlice(err, errStrings) { errStrings = append(errStrings, err) diff --git a/internal/configuration/validator/secrets.go b/internal/configuration/validator/secrets.go index cd6305808..244022aef 100644 --- a/internal/configuration/validator/secrets.go +++ b/internal/configuration/validator/secrets.go @@ -10,39 +10,59 @@ import ( "github.com/authelia/authelia/internal/configuration/schema" ) +// SecretNameToEnvName converts a secret name into the env name. +func SecretNameToEnvName(secretName string) (envName string) { + return "authelia." + secretName + ".file" +} + +func isSecretKey(value string) (isSecretKey bool) { + for _, secretKey := range SecretNames { + if value == secretKey || value == SecretNameToEnvName(secretKey) { + return true + } + } + + return false +} + // ValidateSecrets checks that secrets are either specified by config file/env or by file references. func ValidateSecrets(configuration *schema.Configuration, validator *schema.StructValidator, viper *viper.Viper) { - configuration.JWTSecret = getSecretValue("jwt_secret", validator, viper) - configuration.Session.Secret = getSecretValue("session.secret", validator, viper) + configuration.JWTSecret = getSecretValue(SecretNames["JWTSecret"], validator, viper) + configuration.Session.Secret = getSecretValue(SecretNames["SessionSecret"], validator, viper) if configuration.DuoAPI != nil { - configuration.DuoAPI.SecretKey = getSecretValue("duo_api.secret_key", validator, viper) + configuration.DuoAPI.SecretKey = getSecretValue(SecretNames["DUOSecretKey"], validator, viper) } if configuration.Session.Redis != nil { - configuration.Session.Redis.Password = getSecretValue("session.redis.password", validator, viper) + configuration.Session.Redis.Password = getSecretValue(SecretNames["RedisPassword"], validator, viper) + + if configuration.Session.Redis.HighAvailability != nil { + configuration.Session.Redis.HighAvailability.SentinelPassword = + getSecretValue(SecretNames["RedisSentinelPassword"], validator, viper) + } } if configuration.AuthenticationBackend.Ldap != nil { - configuration.AuthenticationBackend.Ldap.Password = getSecretValue("authentication_backend.ldap.password", validator, viper) + configuration.AuthenticationBackend.Ldap.Password = getSecretValue(SecretNames["LDAPPassword"], validator, viper) } if configuration.Notifier != nil && configuration.Notifier.SMTP != nil { - configuration.Notifier.SMTP.Password = getSecretValue("notifier.smtp.password", validator, viper) + configuration.Notifier.SMTP.Password = getSecretValue(SecretNames["SMTPPassword"], validator, viper) } if configuration.Storage.MySQL != nil { - configuration.Storage.MySQL.Password = getSecretValue("storage.mysql.password", validator, viper) + configuration.Storage.MySQL.Password = getSecretValue(SecretNames["MySQLPassword"], validator, viper) } if configuration.Storage.PostgreSQL != nil { - configuration.Storage.PostgreSQL.Password = getSecretValue("storage.postgres.password", validator, viper) + configuration.Storage.PostgreSQL.Password = getSecretValue(SecretNames["PostgreSQLPassword"], validator, viper) } } func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string { configValue := viper.GetString(name) - fileEnvValue := viper.GetString("authelia." + name + ".file") + fileEnvValue := viper.GetString(SecretNameToEnvName(name)) // Error Checking. if fileEnvValue != "" && configValue != "" { diff --git a/internal/configuration/validator/secrets_test.go b/internal/configuration/validator/secrets_test.go new file mode 100644 index 000000000..6d180dfde --- /dev/null +++ b/internal/configuration/validator/secrets_test.go @@ -0,0 +1,18 @@ +package validator + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShouldValidateCorrectSecretKeys(t *testing.T) { + assert.True(t, isSecretKey("jwt_secret")) + assert.True(t, isSecretKey("authelia.jwt_secret.file")) + assert.False(t, isSecretKey("totp.issuer")) +} + +func TestShouldCreateCorrectSecretEnvNames(t *testing.T) { + assert.Equal(t, "authelia.jwt_secret.file", SecretNameToEnvName("jwt_secret")) + assert.Equal(t, "authelia.not_a_real_secret.file", SecretNameToEnvName("not_a_real_secret")) +} diff --git a/internal/configuration/validator/server_test.go b/internal/configuration/validator/server_test.go index dee5b5746..25c1bde71 100644 --- a/internal/configuration/validator/server_test.go +++ b/internal/configuration/validator/server_test.go @@ -18,6 +18,18 @@ func TestShouldSetDefaultConfig(t *testing.T) { assert.Equal(t, defaultWriteBufferSize, config.WriteBufferSize) } +func TestShouldParsePathCorrectly(t *testing.T) { + validator := schema.NewStructValidator() + config := schema.ServerConfiguration{ + Path: "apple", + } + + ValidateServer(&config, validator) + require.Len(t, validator.Errors(), 0) + + assert.Equal(t, "/apple", config.Path) +} + func TestShouldRaiseOnNegativeValues(t *testing.T) { validator := schema.NewStructValidator() config := schema.ServerConfiguration{ diff --git a/internal/configuration/validator/storage.go b/internal/configuration/validator/storage.go index f0033a174..d1596757e 100644 --- a/internal/configuration/validator/storage.go +++ b/internal/configuration/validator/storage.go @@ -24,11 +24,11 @@ func ValidateStorage(configuration schema.StorageConfiguration, validator *schem func validateSQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) { if configuration.Password == "" || configuration.Username == "" { - validator.Push(errors.New("Username and password must be provided")) + validator.Push(errors.New("the SQL username and password must be provided")) } if configuration.Database == "" { - validator.Push(errors.New("A database must be provided")) + validator.Push(errors.New("the SQL database must be provided")) } } diff --git a/internal/configuration/validator/storage_test.go b/internal/configuration/validator/storage_test.go index a9f442004..dc2550924 100644 --- a/internal/configuration/validator/storage_test.go +++ b/internal/configuration/validator/storage_test.go @@ -55,8 +55,8 @@ func (suite *StorageSuite) TestShouldValidateSQLUsernamePasswordAndDatabaseArePr ValidateStorage(suite.configuration, suite.validator) suite.Require().Len(suite.validator.Errors(), 2) - suite.Assert().EqualError(suite.validator.Errors()[0], "Username and password must be provided") - suite.Assert().EqualError(suite.validator.Errors()[1], "A database must be provided") + suite.Assert().EqualError(suite.validator.Errors()[0], "the SQL username and password must be provided") + suite.Assert().EqualError(suite.validator.Errors()[1], "the SQL database must be provided") suite.validator.Clear() suite.configuration.MySQL = &schema.MySQLStorageConfiguration{ diff --git a/internal/utils/certificates_test.go b/internal/utils/certificates_test.go index 4bc5d2bdf..16af21b67 100644 --- a/internal/utils/certificates_test.go +++ b/internal/utils/certificates_test.go @@ -2,6 +2,7 @@ package utils import ( "crypto/tls" + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -78,15 +79,34 @@ func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) { func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) { pool, errs, nonFatalErrs := NewX509CertPool("/tmp/asdfzyxabc123/not/a/real/dir", nil) assert.NotNil(t, pool) - assert.Len(t, nonFatalErrs, 0) + + if runtime.GOOS == windows { + require.Len(t, nonFatalErrs, 1) + assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows") + } else { + assert.Len(t, nonFatalErrs, 0) + } + require.Len(t, errs, 1) - assert.EqualError(t, errs[0], "could not read certificates from directory open /tmp/asdfzyxabc123/not/a/real/dir: no such file or directory") + + if runtime.GOOS == windows { + assert.EqualError(t, errs[0], "could not read certificates from directory open /tmp/asdfzyxabc123/not/a/real/dir: The system cannot find the path specified.") + } else { + assert.EqualError(t, errs[0], "could not read certificates from directory open /tmp/asdfzyxabc123/not/a/real/dir: no such file or directory") + } } func TestShouldNotReturnErrWhenX509DirectoryExist(t *testing.T) { pool, errs, nonFatalErrs := NewX509CertPool("/tmp", nil) assert.NotNil(t, pool) - assert.Len(t, nonFatalErrs, 0) + + if runtime.GOOS == windows { + require.Len(t, nonFatalErrs, 1) + assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows") + } else { + assert.Len(t, nonFatalErrs, 0) + } + assert.Len(t, errs, 0) } @@ -101,10 +121,20 @@ func TestShouldRaiseNonFatalErrWhenNotifierTrustedCertConfigured(t *testing.T) { pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config) assert.NotNil(t, pool) - require.Len(t, nonFatalErrs, 1) - assert.Len(t, errs, 0) - assert.EqualError(t, nonFatalErrs[0], "defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead") + index := 0 + + if runtime.GOOS == windows { + require.Len(t, nonFatalErrs, 2) + assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows") + + index = 1 + } else { + require.Len(t, nonFatalErrs, 1) + } + + assert.Len(t, errs, 0) + assert.EqualError(t, nonFatalErrs[index], "defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead") } func TestShouldRaiseErrAndNonFatalErrWhenNotifierTrustedCertConfiguredAndNotExist(t *testing.T) { @@ -118,17 +148,35 @@ func TestShouldRaiseErrAndNonFatalErrWhenNotifierTrustedCertConfiguredAndNotExis pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config) assert.NotNil(t, pool) - require.Len(t, nonFatalErrs, 1) + + index := 0 + + if runtime.GOOS == windows { + require.Len(t, nonFatalErrs, 2) + assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows") + + index = 1 + } else { + require.Len(t, nonFatalErrs, 1) + } + require.Len(t, errs, 1) assert.EqualError(t, errs[0], "could not import legacy SMTP trusted_cert (see the new certificates_directory option) certificate /tmp/asdfzyxabc123/not/a/real/cert.pem (file does not exist)") - assert.EqualError(t, nonFatalErrs[0], "defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead") + assert.EqualError(t, nonFatalErrs[index], "defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead") } func TestShouldReadCertsFromDirectoryButNotKeys(t *testing.T) { pool, errs, nonFatalErrs := NewX509CertPool("../suites/common/ssl/", nil) assert.NotNil(t, pool) require.Len(t, errs, 1) - assert.Len(t, nonFatalErrs, 0) + + if runtime.GOOS == "windows" { + require.Len(t, nonFatalErrs, 1) + assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows") + } else { + assert.Len(t, nonFatalErrs, 0) + } + assert.EqualError(t, errs[0], "could not import certificate key.pem") } diff --git a/internal/utils/const.go b/internal/utils/const.go index fa10d3688..58f7f7a44 100644 --- a/internal/utils/const.go +++ b/internal/utils/const.go @@ -25,6 +25,8 @@ const Year = Day * 365 // Month is an int based representation of the time unit. const Month = Year / 12 +const windows = "windows" + // RFC3339Zero is the default value for time.Time.Unix(). const RFC3339Zero = int64(-62135596800)