From b9fb33d8069298cbd385fa9118095d185fc2e6f1 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 23 Apr 2020 11:11:32 +1000 Subject: [PATCH] [FEATURE] File Secrets (#896) * [FEATURE] File Secret Loading * add a validator for secrets * run the secrets validator before the main config validator * only allow a secret to be defined in one of: config, env, file env * remove LF if found in file * update configuration before main config validation * fix unit tests * implement secret testing * refactor the secrets validator * make check os agnostic * update docs * add warning when user attempts to use ENV instead of ENV file * discourage ENV in docs * update config template * oxford comma * apply suggestions from code review * rename Validate to ValidateConfiguration * add k8s example * add deprecation notice in docs and warning * style changes --- config.template.yml | 19 +- docs/configuration/authentication/ldap.md | 10 +- docs/configuration/miscellaneous.md | 2 +- docs/configuration/notifier/smtp.md | 8 +- docs/configuration/secrets.md | 196 ++++++++++++++++-- docs/configuration/session.md | 4 +- docs/configuration/storage/mariadb.md | 6 +- docs/configuration/storage/mysql.md | 6 +- docs/configuration/storage/postgres.md | 15 +- internal/configuration/reader.go | 29 ++- internal/configuration/reader_test.go | 64 +++++- .../test_resources/config_alt.yml | 123 +++++++++++ .../test_resources/config_with_secret.yml | 124 +++++++++++ .../configuration/validator/configuration.go | 4 +- .../validator/configuration_test.go | 24 +-- internal/configuration/validator/secrets.go | 71 +++++++ internal/configuration/validator/storage.go | 2 +- .../configuration/validator/storage_test.go | 2 +- 18 files changed, 649 insertions(+), 60 deletions(-) create mode 100644 internal/configuration/test_resources/config_alt.yml create mode 100644 internal/configuration/test_resources/config_with_secret.yml create mode 100644 internal/configuration/validator/secrets.go diff --git a/config.template.yml b/config.template.yml index 6489dda85..de79839ab 100644 --- a/config.template.yml +++ b/config.template.yml @@ -15,7 +15,7 @@ log_level: debug # The secret used to generate JWT tokens when validating user identity by # email confirmation. -# This secret can also be set using the env variables AUTHELIA_JWT_SECRET +# JWT Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html jwt_secret: a_very_important_secret # Default redirection URL @@ -58,7 +58,7 @@ totp: duo_api: hostname: api-123456789.example.com integration_key: ABCDEF - # This secret can also be set using the env variables AUTHELIA_DUO_API_SECRET_KEY + # Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html secret_key: 1234567890abcdefghifjkl # The authentication backend to use for verifying user passwords @@ -138,7 +138,7 @@ authentication_backend: # The username and password of the admin user. user: cn=admin,dc=example,dc=com - # This secret can also be set using the env variables AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: password # File backend configuration. @@ -271,7 +271,7 @@ session: name: authelia_session # The secret to encrypt the session data. This is only used with Redis. - # This secret can also be set using the env variables AUTHELIA_SESSION_SECRET + # Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html secret: insecure_session_secret # The time in seconds before the cookie expires and session is reset. @@ -296,7 +296,7 @@ session: redis: host: 127.0.0.1 port: 6379 - # This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: authelia # This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc). database_index: 0 @@ -334,7 +334,7 @@ storage: port: 3306 database: authelia username: authelia - # This secret can also be set using the env variables AUTHELIA_STORAGE_MYSQL_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: mypassword # Settings to connect to PostgreSQL server @@ -343,8 +343,9 @@ storage: # port: 5432 # database: authelia # username: authelia - # # This secret can also be set using the env variables AUTHELIA_STORAGE_POSTGRES_PASSWORD + # # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html # password: mypassword + # sslmode: disable # Configuration of the notification system. # @@ -372,7 +373,7 @@ notifier: # - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure) smtp: username: test - # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: password host: 127.0.0.1 port: 1025 @@ -390,7 +391,7 @@ notifier: # You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en ## smtp: ## username: myaccount@gmail.com - ## # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD + ## # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html ## password: yourapppassword ## sender: admin@example.com ## host: smtp.gmail.com diff --git a/docs/configuration/authentication/ldap.md b/docs/configuration/authentication/ldap.md index 6e708478e..7a9078365 100644 --- a/docs/configuration/authentication/ldap.md +++ b/docs/configuration/authentication/ldap.md @@ -81,13 +81,13 @@ authentication_backend: # one returned by the LDAP server is used. user: cn=admin,dc=example,dc=com - # This secret can also be set using the env variables AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: password ``` The user must have an email address in order for Authelia to perform -identity verification when password reset request is initiated or -when a second factor device is registered. +identity verification when a user attempts to reset their password or +register a second factor device. ## Important notes @@ -99,3 +99,7 @@ In order to avoid such problems, we highly recommended you follow https://www.ie `sAMAccountName` for Microsoft Active Directory and `uid` for other implementations as the attribute holding the unique identifier for your users. +## Loading a password from a secret instead of inside the configuration + +Password can also be defined using a [secret](../secrets.md). + diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 2ac4c4085..42acd2b00 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -64,7 +64,7 @@ log_file_path: /var/log/authelia.log `optional: false` Defines the secret used to craft JWT tokens leveraged by the identity -verification process +verification process. This can also be defined using a [secret](./secrets.md). ```yaml jwt_secret: v3ry_important_s3cr3t diff --git a/docs/configuration/notifier/smtp.md b/docs/configuration/notifier/smtp.md index 5716fe422..2036e1bc5 100644 --- a/docs/configuration/notifier/smtp.md +++ b/docs/configuration/notifier/smtp.md @@ -38,7 +38,7 @@ notifier: # - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure) smtp: username: test - # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: password host: 127.0.0.1 port: 1025 @@ -62,9 +62,13 @@ described [here](https://support.google.com/accounts/answer/185833?hl=en) notifier: smtp: username: myaccount@gmail.com - # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: yourapppassword sender: admin@example.com host: smtp.gmail.com port: 587 ``` + +## Loading a password from a secret instead of inside the configuration + +Password can also be defined using a [secret](../secrets.md). \ No newline at end of file diff --git a/docs/configuration/secrets.md b/docs/configuration/secrets.md index cd25e954c..135cc1076 100644 --- a/docs/configuration/secrets.md +++ b/docs/configuration/secrets.md @@ -16,25 +16,47 @@ below. A secret can be configured using an environment variable with the prefix AUTHELIA_ followed by the path of the option capitalized -and with dots replaced by underscores. +and with dots replaced by underscores followed by the suffix _FILE. -For instance the LDAP password is identified by the path -**authentication_backend.ldap.password**, so this password could -alternatively be set using the environment variable called -**AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD**. +The contents of the environment variable must be a path to a file +containing the secret data. This file must be readable by the +user the Authelia daemon is running as. + +For instance the LDAP password can be defined in the configuration +at the path **authentication_backend.ldap.password**, so this password +could alternatively be set using the environment variable called +**AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE**. 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. -* 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 +|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| + + +## Secrets exposed in an environment variable + +Prior to implementing file secrets you were able to define the +values of secrets in the environment variables themselves +in plain text instead of referencing a file. This is still +supported but discouraged. If you still want to do this +just remove _FILE from the environment variable name +and define the value in insecure plain text. See +[this article](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/) +for reasons why this is considered insecure and is discouraged. + +**DEPRECATION NOTICE:** This backwards compatibility feature will be +**removed** in 4.18.0+. + ## Secrets in configuration file @@ -42,3 +64,151 @@ If for some reason you prefer keeping the secrets in the configuration file, be sure to apply the right permissions to the file in order to prevent secret leaks if an another application gets compromised on your server. The UNIX permissions should probably be something like 600. + + +## Kubernetes + +Secrets can be mounted as files using the following sample manifests. + + +### Kustomization + +- **Filename:** ./kustomization.yaml +- **Command:** kubectl apply -k +- **Notes:** this kustomization expects the Authelia configuration.yml in +the same directory. You will need to edit the kustomization.yaml with your +desired secrets after the equal signs. If you change the value before the +equal sign you'll have to adjust the volumes section of the daemonset +template (or deployment template if you're using it). + +```yaml +#filename: ./kustomization.yaml +generatorOptions: + disableNameSuffixHash: true + labels: + type: generated + app: authelia +configMapGenerator: + - name: authelia + files: + - configuration.yml +secretGenerator: + - name: authelia + literals: + - jwt_secret=myverysecuresecret + - session_secret=mysessionsecret + - redis_password=myredispassword + - sql_password=mysqlpassword + - ldap_password=myldappassword + - duo_secret=myduosecretkey + - smtp_password=mysmtppassword +``` + +### DaemonSet + +- **Filename:** ./daemonset.yaml +- **Command:** kubectl apply -f ./daemonset.yaml +- **Notes:** assumes Kubernetes API 1.16 or greater +```yaml +#filename: daemonset.yaml +#command: kubectl apply -f daemonset.yaml +#notes: assumes kubernetes api 1.16+ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: authelia + labels: + app: authelia +spec: + selector: + matchLabels: + app: authelia + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: authelia + spec: + containers: + - name: authelia + image: authelia/authelia:latest + imagePullPolicy: IfNotPresent + env: + - name: AUTHELIA_JWT_SECRET_FILE + value: /usr/app/secrets/jwt + - name: AUTHELIA_DUO_API_SECRET_KEY_FILE + value: /usr/app/secrets/duo + - name: AUTHELIA_SESSION_SECRET_FILE + value: /usr/app/secrets/session + - name: AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE + value: /usr/app/secrets/ldap_password + - name: AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE + value: /usr/app/secrets/smtp_password + - name: AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE + value: /usr/app/secrets/sql_password + ports: + - name: http + containerPort: 80 + startupProbe: + httpGet: + path: /api/configuration + port: http + initialDelaySeconds: 10 + timeoutSeconds: 5 + periodSeconds: 5 + failureThreshold: 4 + livenessProbe: + httpGet: + path: /api/configuration + port: http + initialDelaySeconds: 60 + timeoutSeconds: 5 + periodSeconds: 30 + failureThreshold: 2 + readinessProbe: + httpGet: + path: /api/configuration + port: http + initialDelaySeconds: 10 + timeoutSeconds: 5 + periodSeconds: 5 + failureThreshold: 5 + volumeMounts: + - mountPath: /etc/authelia + name: config-volume + - mountPath: /usr/app/secrets + name: secrets + readOnly: true + - mountPath: /etc/localtime + name: localtime + readOnly: true + volumes: + - name: config-volume + configMap: + name: authelia + items: + - key: configuration.yml + path: configuration.yml + - name: secrets + secret: + secretName: authelia + items: + - key: jwt_secret + path: jwt + - key: duo_secret + path: duo + - key: session_secret + path: session + - key: redis_password + path: redis_password + - key: sql_password + path: sql_password + - key: ldap_password + path: ldap_password + - key: smtp_password + path: smtp_password + - name: localtime + hostPath: + path: /etc/localtime +``` \ No newline at end of file diff --git a/docs/configuration/session.md b/docs/configuration/session.md index 9d2a21f12..41790b7a2 100644 --- a/docs/configuration/session.md +++ b/docs/configuration/session.md @@ -23,7 +23,7 @@ session: name: authelia_session # The secret to encrypt the session cookie. - # This secret can also be set using the env variables AUTHELIA_SESSION_SECRET + # Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html secret: unsecure_session_secret # The time in seconds before the cookie expires and session is reset. @@ -48,7 +48,7 @@ session: redis: host: 127.0.0.1 port: 6379 - # This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: authelia ``` diff --git a/docs/configuration/storage/mariadb.md b/docs/configuration/storage/mariadb.md index ff7f85bc7..7cd4c23b0 100644 --- a/docs/configuration/storage/mariadb.md +++ b/docs/configuration/storage/mariadb.md @@ -15,6 +15,10 @@ storage: port: 3306 database: authelia username: authelia - # This secret can also be set using the env variables AUTHELIA_STORAGE_MYSQL_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: mypassword ``` + +## Loading a password from a secret instead of inside the configuration + +Password can also be defined using a [secret](../secrets.md). diff --git a/docs/configuration/storage/mysql.md b/docs/configuration/storage/mysql.md index 486b17ebd..72f4b021e 100644 --- a/docs/configuration/storage/mysql.md +++ b/docs/configuration/storage/mysql.md @@ -15,6 +15,10 @@ storage: port: 3306 database: authelia username: authelia - # This secret can also be set using the env variables AUTHELIA_STORAGE_MYSQL_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: mypassword ``` + +## Loading a password from a secret instead of inside the configuration + +Password can also be defined using a [secret](../secrets.md). \ No newline at end of file diff --git a/docs/configuration/storage/postgres.md b/docs/configuration/storage/postgres.md index acff43da9..56f8403ea 100644 --- a/docs/configuration/storage/postgres.md +++ b/docs/configuration/storage/postgres.md @@ -15,6 +15,19 @@ storage: port: 5432 database: authelia username: authelia - # This secret can also be set using the env variables AUTHELIA_STORAGE_POSTGRES_PASSWORD + # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html password: mypassword + sslmode: disable ``` + +## SSL Mode + +SSL mode configures how to handle SSL connections with Postgres. +Valid options are 'disable', 'require', 'verify-ca', or 'verify-full'. +See the [PostgreSQL Documentation](https://www.postgresql.org/docs/12/libpq-ssl.html) +or [Pure Go Postgres driver Documentation](https://godoc.org/github.com/lib/pq) +for more information. + +## Loading a password from a secret instead of inside the configuration + +Password can also be defined using a [secret](../secrets.md). \ No newline at end of file diff --git a/internal/configuration/reader.go b/internal/configuration/reader.go index 4a86522f1..f8c305ae5 100644 --- a/internal/configuration/reader.go +++ b/internal/configuration/reader.go @@ -12,19 +12,27 @@ import ( // Read a YAML configuration and create a Configuration object out of it. func Read(configPath string) (*schema.Configuration, []error) { - viper.SetEnvPrefix("AUTHELIA") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // we need to bind all env variables as long as https://github.com/spf13/viper/issues/761 // is not resolved. - viper.BindEnv("jwt_secret") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("duo_api.secret_key") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("session.secret") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("authentication_backend.ldap.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("notifier.smtp.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("session.redis.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("storage.mysql.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. - viper.BindEnv("storage.postgres.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.jwt_secret") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.duo_api.secret_key") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.session.secret") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.authentication_backend.ldap.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.notifier.smtp.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.session.redis.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.storage.mysql.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + viper.BindEnv("authelia.storage.postgres.password") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. + + 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. viper.SetConfigFile(configPath) @@ -38,7 +46,8 @@ func Read(configPath string) (*schema.Configuration, []error) { viper.Unmarshal(&configuration) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. val := schema.NewStructValidator() - validator.Validate(&configuration, val) + validator.ValidateSecrets(&configuration, val, viper.GetViper()) + validator.ValidateConfiguration(&configuration, val) if val.HasErrors() { return nil, val.Errors() diff --git a/internal/configuration/reader_test.go b/internal/configuration/reader_test.go index b21c8802d..0bc6cffaa 100644 --- a/internal/configuration/reader_test.go +++ b/internal/configuration/reader_test.go @@ -2,12 +2,25 @@ package configuration import ( "os" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func resetEnv() { + _ = os.Unsetenv("AUTHELIA_JWT_SECRET") + _ = os.Unsetenv("AUTHELIA_DUO_API_SECRET_KEY") + _ = os.Unsetenv("AUTHELIA_SESSION_SECRET") + _ = os.Unsetenv("AUTHELIA_SESSION_SECRET") + _ = os.Unsetenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD") + _ = os.Unsetenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD") + _ = os.Unsetenv("AUTHELIA_SESSION_REDIS_PASSWORD") + _ = os.Unsetenv("AUTHELIA_STORAGE_MYSQL_PASSWORD") + _ = os.Unsetenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD") +} + func TestShouldParseConfigFile(t *testing.T) { require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET", "secret_from_env")) require.NoError(t, os.Setenv("AUTHELIA_DUO_API_SECRET_KEY", "duo_secret_from_env")) @@ -16,7 +29,6 @@ func TestShouldParseConfigFile(t *testing.T) { require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD", "smtp_secret_from_env")) require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD", "redis_secret_from_env")) require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD", "mysql_secret_from_env")) - require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD", "postgres_secret_from_env")) config, errors := Read("./test_resources/config.yml") @@ -37,8 +49,58 @@ func TestShouldParseConfigFile(t *testing.T) { 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, "mysql_secret_from_env", config.Storage.MySQL.Password) + + assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) + assert.Len(t, config.AccessControl.Rules, 12) +} + +func TestShouldParseAltConfigFile(t *testing.T) { + require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD", "postgres_secret_from_env")) + config, errors := Read("./test_resources/config_alt.yml") + require.Len(t, errors, 0) + + assert.Equal(t, 9091, config.Port) + assert.Equal(t, "debug", config.LogLevel) + assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL) + assert.Equal(t, "authelia.com", config.TOTP.Issuer) + assert.Equal(t, "secret_from_env", config.JWTSecret) + + assert.Equal(t, "api-123456789.example.com", config.DuoAPI.Hostname) + assert.Equal(t, "ABCDEF", config.DuoAPI.IntegrationKey) assert.Equal(t, "postgres_secret_from_env", config.Storage.PostgreSQL.Password) assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) assert.Len(t, config.AccessControl.Rules, 12) } + +func TestShouldOnlyAllowOneEnvType(t *testing.T) { + resetEnv() + require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD", "postgres_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", "/tmp/postgres_secret")) + require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET", "secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_DUO_API_SECRET_KEY", "duo_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_SECRET", "session_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD", "ldap_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD", "smtp_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD", "redis_secret_from_env")) + _, errors := Read("./test_resources/config_alt.yml") + + require.Len(t, errors, 2) + assert.EqualError(t, errors[0], "secret is defined in multiple areas: storage.postgres.password") + assert.True(t, strings.HasPrefix(errors[1].Error(), "error loading secret file (storage.postgres.password): open /tmp/postgres_secret: ")) +} + +func TestShouldOnlyAllowEnvOrConfig(t *testing.T) { + resetEnv() + require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD", "mysql_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET", "secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_DUO_API_SECRET_KEY", "duo_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_SECRET", "session_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD", "ldap_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD", "smtp_secret_from_env")) + require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD", "redis_secret_from_env")) + _, errors := Read("./test_resources/config_with_secret.yml") + + require.Len(t, errors, 1) + require.EqualError(t, errors[0], "error loading secret (jwt_secret): it's already defined in the config file") +} diff --git a/internal/configuration/test_resources/config_alt.yml b/internal/configuration/test_resources/config_alt.yml new file mode 100644 index 000000000..84cda028e --- /dev/null +++ b/internal/configuration/test_resources/config_alt.yml @@ -0,0 +1,123 @@ +############################################################### +# Authelia configuration # +############################################################### + +host: 127.0.0.1 +port: 9091 + +log_level: debug +default_redirection_url: https://home.example.com:8080/ + +totp: + issuer: authelia.com + +duo_api: + hostname: api-123456789.example.com + integration_key: ABCDEF + +authentication_backend: + ldap: + url: ldap://127.0.0.1 + base_dn: dc=example,dc=com + username_attribute: uid + additional_users_dn: ou=users + users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user)) + additional_groups_dn: ou=groups + groups_filter: (&(member={dn})(objectclass=groupOfNames)) + group_name_attribute: cn + mail_attribute: mail + user: cn=admin,dc=example,dc=com + +access_control: + default_policy: deny + + rules: + # Rules applied to everyone + - domain: public.example.com + policy: bypass + + - domain: secure.example.com + policy: one_factor + # Network based rule, if not provided any network matches. + networks: + - 192.168.1.0/24 + - domain: secure.example.com + policy: two_factor + + - domain: [singlefactor.example.com, onefactor.example.com] + policy: one_factor + + # Rules applied to 'admins' group + - domain: "mx2.mail.example.com" + subject: "group:admins" + policy: deny + - domain: "*.example.com" + subject: "group:admins" + policy: two_factor + + # Rules applied to 'dev' group + - domain: dev.example.com + resources: + - "^/groups/dev/.*$" + subject: "group:dev" + policy: two_factor + + # Rules applied to user 'john' + - domain: dev.example.com + resources: + - "^/users/john/.*$" + subject: "user:john" + policy: two_factor + + # Rules applied to 'dev' group and user 'john' + - domain: dev.example.com + resources: + - "^/deny-all.*$" + subject: ["group:dev", "user:john"] + policy: denied + + # Rules applied to user 'harry' + - domain: dev.example.com + resources: + - "^/users/harry/.*$" + subject: "user:harry" + policy: two_factor + + # Rules applied to user 'bob' + - domain: "*.mail.example.com" + subject: "user:bob" + policy: two_factor + - domain: "dev.example.com" + resources: + - "^/users/bob/.*$" + subject: "user:bob" + policy: two_factor + +session: + name: authelia_session + expiration: 3600000 # 1 hour + inactivity: 300000 # 5 minutes + domain: example.com + redis: + host: 127.0.0.1 + port: 6379 + +regulation: + max_retries: 3 + find_time: 120 + ban_time: 300 + +storage: + postgres: + host: 127.0.0.1 + port: 3306 + database: authelia + username: authelia + +notifier: + smtp: + username: test + host: 127.0.0.1 + port: 1025 + sender: admin@example.com + disable_require_tls: true \ No newline at end of file diff --git a/internal/configuration/test_resources/config_with_secret.yml b/internal/configuration/test_resources/config_with_secret.yml new file mode 100644 index 000000000..26f999597 --- /dev/null +++ b/internal/configuration/test_resources/config_with_secret.yml @@ -0,0 +1,124 @@ +############################################################### +# Authelia configuration # +############################################################### + +host: 127.0.0.1 +port: 9091 +jwt_secret: secret_from_config + +log_level: debug +default_redirection_url: https://home.example.com:8080/ + +totp: + issuer: authelia.com + +duo_api: + hostname: api-123456789.example.com + integration_key: ABCDEF + +authentication_backend: + ldap: + url: ldap://127.0.0.1 + base_dn: dc=example,dc=com + username_attribute: uid + additional_users_dn: ou=users + users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user)) + additional_groups_dn: ou=groups + groups_filter: (&(member={dn})(objectclass=groupOfNames)) + group_name_attribute: cn + mail_attribute: mail + user: cn=admin,dc=example,dc=com + +access_control: + default_policy: deny + + rules: + # Rules applied to everyone + - domain: public.example.com + policy: bypass + + - domain: secure.example.com + policy: one_factor + # Network based rule, if not provided any network matches. + networks: + - 192.168.1.0/24 + - domain: secure.example.com + policy: two_factor + + - domain: [singlefactor.example.com, onefactor.example.com] + policy: one_factor + + # Rules applied to 'admins' group + - domain: "mx2.mail.example.com" + subject: "group:admins" + policy: deny + - domain: "*.example.com" + subject: "group:admins" + policy: two_factor + + # Rules applied to 'dev' group + - domain: dev.example.com + resources: + - "^/groups/dev/.*$" + subject: "group:dev" + policy: two_factor + + # Rules applied to user 'john' + - domain: dev.example.com + resources: + - "^/users/john/.*$" + subject: "user:john" + policy: two_factor + + # Rules applied to 'dev' group and user 'john' + - domain: dev.example.com + resources: + - "^/deny-all.*$" + subject: ["group:dev", "user:john"] + policy: denied + + # Rules applied to user 'harry' + - domain: dev.example.com + resources: + - "^/users/harry/.*$" + subject: "user:harry" + policy: two_factor + + # Rules applied to user 'bob' + - domain: "*.mail.example.com" + subject: "user:bob" + policy: two_factor + - domain: "dev.example.com" + resources: + - "^/users/bob/.*$" + subject: "user:bob" + policy: two_factor + +session: + name: authelia_session + expiration: 3600000 # 1 hour + inactivity: 300000 # 5 minutes + domain: example.com + redis: + host: 127.0.0.1 + port: 6379 + +regulation: + max_retries: 3 + find_time: 120 + ban_time: 300 + +storage: + mysql: + host: 127.0.0.1 + port: 3306 + database: authelia + username: authelia + +notifier: + smtp: + username: test + host: 127.0.0.1 + port: 1025 + sender: admin@example.com + disable_require_tls: true \ No newline at end of file diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go index 0ecc2ea9c..e43777526 100644 --- a/internal/configuration/validator/configuration.go +++ b/internal/configuration/validator/configuration.go @@ -10,8 +10,8 @@ import ( var defaultPort = 8080 var defaultLogLevel = "info" -// Validate and adapt the configuration read from file. -func Validate(configuration *schema.Configuration, validator *schema.StructValidator) { +// ValidateConfiguration and adapt the configuration read from file. +func ValidateConfiguration(configuration *schema.Configuration, validator *schema.StructValidator) { if configuration.Host == "" { configuration.Host = "0.0.0.0" } diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index c2e3eb321..a5fb1897d 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -37,7 +37,7 @@ func TestShouldNotUpdateConfig(t *testing.T) { validator := schema.NewStructValidator() config := newDefaultConfig() - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) assert.Equal(t, 9090, config.Port) @@ -49,7 +49,7 @@ func TestShouldValidateAndUpdatePort(t *testing.T) { config := newDefaultConfig() config.Port = 0 - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) assert.Equal(t, 8080, config.Port) @@ -60,7 +60,7 @@ func TestShouldValidateAndUpdateHost(t *testing.T) { config := newDefaultConfig() config.Host = "" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) assert.Equal(t, "0.0.0.0", config.Host) @@ -71,7 +71,7 @@ func TestShouldValidateAndUpdateLogsLevel(t *testing.T) { config := newDefaultConfig() config.LogLevel = "" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) assert.Equal(t, "info", config.LogLevel) @@ -81,12 +81,12 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) { validator := schema.NewStructValidator() config := newDefaultConfig() - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) config.Notifier = nil - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], "A notifier configuration must be provided") } @@ -95,7 +95,7 @@ func TestShouldAddDefaultAccessControl(t *testing.T) { validator := schema.NewStructValidator() config := newDefaultConfig() - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) assert.NotNil(t, config.AccessControl) assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) @@ -106,7 +106,7 @@ func TestShouldRaiseErrorWhenTLSCertWithoutKeyIsProvided(t *testing.T) { config := newDefaultConfig() config.TLSCert = "/tmp/cert.pem" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], "No TLS key provided, please check the \"tls_key\" which has been configured") } @@ -116,7 +116,7 @@ func TestShouldRaiseErrorWhenTLSKeyWithoutCertIsProvided(t *testing.T) { config := newDefaultConfig() config.TLSKey = "/tmp/key.pem" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], "No TLS certificate provided, please check the \"tls_cert\" which has been configured") } @@ -127,7 +127,7 @@ func TestShouldNotRaiseErrorWhenBothTLSCertificateAndKeyAreProvided(t *testing.T config.TLSCert = "/tmp/cert.pem" config.TLSKey = "/tmp/key.pem" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) } @@ -136,7 +136,7 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) { config := newDefaultConfig() config.JWTSecret = "" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], "Provide a JWT secret using \"jwt_secret\" key") } @@ -146,7 +146,7 @@ func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) { config := newDefaultConfig() config.DefaultRedirectionURL = "abc" - Validate(&config, validator) + ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], "Unable to parse default redirection url") } diff --git a/internal/configuration/validator/secrets.go b/internal/configuration/validator/secrets.go new file mode 100644 index 000000000..66af45363 --- /dev/null +++ b/internal/configuration/validator/secrets.go @@ -0,0 +1,71 @@ +package validator + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/spf13/viper" + + "github.com/authelia/authelia/internal/configuration/schema" + "github.com/authelia/authelia/internal/logging" +) + +// 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) + + if configuration.DuoAPI != nil { + configuration.DuoAPI.SecretKey = getSecretValue("duo_api.secret_key", validator, viper) + } + + if configuration.Session.Redis != nil { + configuration.Session.Redis.Password = getSecretValue("session.redis.password", validator, viper) + } + + if configuration.AuthenticationBackend.Ldap != nil { + configuration.AuthenticationBackend.Ldap.Password = getSecretValue("authentication_backend.ldap.password", validator, viper) + } + + if configuration.Notifier != nil && configuration.Notifier.SMTP != nil { + configuration.Notifier.SMTP.Password = getSecretValue("notifier.smtp.password", validator, viper) + } + + if configuration.Storage.MySQL != nil { + configuration.Storage.MySQL.Password = getSecretValue("storage.mysql.password", validator, viper) + } + + if configuration.Storage.PostgreSQL != nil { + configuration.Storage.PostgreSQL.Password = getSecretValue("storage.postgres.password", validator, viper) + } +} + +func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string { + configValue := viper.GetString(name) + envValue := viper.GetString("authelia." + name) + fileEnvValue := viper.GetString("authelia." + name + ".file") + + // Error Checking. + if envValue != "" && fileEnvValue != "" { + validator.Push(fmt.Errorf("secret is defined in multiple areas: %s", name)) + } + if (envValue != "" || fileEnvValue != "") && configValue != "" { + validator.Push(fmt.Errorf("error loading secret (%s): it's already defined in the config file", name)) + } + + // Derive Secret. + if fileEnvValue != "" { + content, err := ioutil.ReadFile(fileEnvValue) + if err != nil { + validator.Push(fmt.Errorf("error loading secret file (%s): %s", name, err)) + } else { + return strings.Replace(string(content), "\n", "", -1) + } + } + if envValue != "" { + logging.Logger().Warnf("The following secret is defined as an environment variable, this is insecure and being removed in 4.18.0+, it's recommended to use the file secrets instead (https://docs.authelia.com/configuration/secrets.html): %s", name) + return envValue + } + return configValue +} diff --git a/internal/configuration/validator/storage.go b/internal/configuration/validator/storage.go index 25bff51de..60b39f3e4 100644 --- a/internal/configuration/validator/storage.go +++ b/internal/configuration/validator/storage.go @@ -40,7 +40,7 @@ func validatePostgreSQLConfiguration(configuration *schema.PostgreSQLStorageConf if !(configuration.SSLMode == "disable" || configuration.SSLMode == "require" || configuration.SSLMode == "verify-ca" || configuration.SSLMode == "verify-full") { - validator.Push(errors.New("SSL mode must be 'disable', 'require', 'verify-ca' or 'verify-full'")) + validator.Push(errors.New("SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'")) } } diff --git a/internal/configuration/validator/storage_test.go b/internal/configuration/validator/storage_test.go index ba8183039..952235e42 100644 --- a/internal/configuration/validator/storage_test.go +++ b/internal/configuration/validator/storage_test.go @@ -94,7 +94,7 @@ func (s *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() { ValidateStorage(s.configuration, validator) s.Require().Len(validator.Errors(), 1) - s.Assert().EqualError(validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca' or 'verify-full'") + s.Assert().EqualError(validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'") } func TestShouldRunStorageSuite(t *testing.T) {