[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
pull/901/head
James Elliott 2020-04-23 11:11:32 +10:00 committed by GitHub
parent 0ec3f18b44
commit b9fb33d806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 649 additions and 60 deletions

View File

@ -15,7 +15,7 @@ log_level: debug
# The secret used to generate JWT tokens when validating user identity by # The secret used to generate JWT tokens when validating user identity by
# email confirmation. # 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 jwt_secret: a_very_important_secret
# Default redirection URL # Default redirection URL
@ -58,7 +58,7 @@ totp:
duo_api: duo_api:
hostname: api-123456789.example.com hostname: api-123456789.example.com
integration_key: ABCDEF 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 secret_key: 1234567890abcdefghifjkl
# The authentication backend to use for verifying user passwords # The authentication backend to use for verifying user passwords
@ -138,7 +138,7 @@ authentication_backend:
# The username and password of the admin user. # The username and password of the admin user.
user: cn=admin,dc=example,dc=com 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 password: password
# File backend configuration. # File backend configuration.
@ -271,7 +271,7 @@ session:
name: authelia_session name: authelia_session
# The secret to encrypt the session data. This is only used with Redis. # 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 secret: insecure_session_secret
# The time in seconds before the cookie expires and session is reset. # The time in seconds before the cookie expires and session is reset.
@ -296,7 +296,7 @@ session:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 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 password: authelia
# This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc). # This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc).
database_index: 0 database_index: 0
@ -334,7 +334,7 @@ storage:
port: 3306 port: 3306
database: authelia database: authelia
username: 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 password: mypassword
# Settings to connect to PostgreSQL server # Settings to connect to PostgreSQL server
@ -343,8 +343,9 @@ storage:
# port: 5432 # port: 5432
# database: authelia # database: authelia
# username: 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 # password: mypassword
# sslmode: disable
# Configuration of the notification system. # 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) # - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure)
smtp: smtp:
username: test 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 password: password
host: 127.0.0.1 host: 127.0.0.1
port: 1025 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 # You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en
## smtp: ## smtp:
## username: myaccount@gmail.com ## 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 ## password: yourapppassword
## sender: admin@example.com ## sender: admin@example.com
## host: smtp.gmail.com ## host: smtp.gmail.com

View File

@ -81,13 +81,13 @@ authentication_backend:
# one returned by the LDAP server is used. # one returned by the LDAP server is used.
user: cn=admin,dc=example,dc=com 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 password: password
``` ```
The user must have an email address in order for Authelia to perform The user must have an email address in order for Authelia to perform
identity verification when password reset request is initiated or identity verification when a user attempts to reset their password or
when a second factor device is registered. register a second factor device.
## Important notes ## 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 `sAMAccountName` for Microsoft Active Directory and `uid` for other implementations as the attribute holding the
unique identifier for your users. 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).

View File

@ -64,7 +64,7 @@ log_file_path: /var/log/authelia.log
`optional: false` `optional: false`
Defines the secret used to craft JWT tokens leveraged by the identity 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 ```yaml
jwt_secret: v3ry_important_s3cr3t jwt_secret: v3ry_important_s3cr3t

View File

@ -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) # - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure)
smtp: smtp:
username: test 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 password: password
host: 127.0.0.1 host: 127.0.0.1
port: 1025 port: 1025
@ -62,9 +62,13 @@ described [here](https://support.google.com/accounts/answer/185833?hl=en)
notifier: notifier:
smtp: smtp:
username: myaccount@gmail.com 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 password: yourapppassword
sender: admin@example.com sender: admin@example.com
host: smtp.gmail.com host: smtp.gmail.com
port: 587 port: 587
``` ```
## Loading a password from a secret instead of inside the configuration
Password can also be defined using a [secret](../secrets.md).

View File

@ -16,25 +16,47 @@ below.
A secret can be configured using an environment variable with the A secret can be configured using an environment variable with the
prefix AUTHELIA_ followed by the path of the option capitalized 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 The contents of the environment variable must be a path to a file
**authentication_backend.ldap.password**, so this password could containing the secret data. This file must be readable by the
alternatively be set using the environment variable called user the Authelia daemon is running as.
**AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD**.
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 Here is the list of the environment variables which are considered
secrets and can be defined. Any other option defined using an secrets and can be defined. Any other option defined using an
environment variable will not be replaced. environment variable will not be replaced.
* AUTHELIA_JWT_SECRET |Configuration Key |Environment Variable |
* AUTHELIA_DUO_API_SECRET_KEY |:----------------------------------:|:------------------------------------------------:|
* AUTHELIA_SESSION_SECRET |jwt_secret |AUTHELIA_JWT_SECRET_FILE |
* AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD |duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE |
* AUTHELIA_NOTIFIER_SMTP_PASSWORD |session.secret |AUTHELIA_SESSION_SECRET_FILE |
* AUTHELIA_SESSION_REDIS_PASSWORD |session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
* AUTHELIA_STORAGE_MYSQL_PASSWORD |storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
* AUTHELIA_STORAGE_POSTGRES_PASSWORD |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 ## 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 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 prevent secret leaks if an another application gets compromised on your
server. The UNIX permissions should probably be something like 600. 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
```

View File

@ -23,7 +23,7 @@ session:
name: authelia_session name: authelia_session
# The secret to encrypt the session cookie. # 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 secret: unsecure_session_secret
# The time in seconds before the cookie expires and session is reset. # The time in seconds before the cookie expires and session is reset.
@ -48,7 +48,7 @@ session:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 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 password: authelia
``` ```

View File

@ -15,6 +15,10 @@ storage:
port: 3306 port: 3306
database: authelia database: authelia
username: 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 password: mypassword
``` ```
## Loading a password from a secret instead of inside the configuration
Password can also be defined using a [secret](../secrets.md).

View File

@ -15,6 +15,10 @@ storage:
port: 3306 port: 3306
database: authelia database: authelia
username: 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 password: mypassword
``` ```
## Loading a password from a secret instead of inside the configuration
Password can also be defined using a [secret](../secrets.md).

View File

@ -15,6 +15,19 @@ storage:
port: 5432 port: 5432
database: authelia database: authelia
username: 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 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).

View File

@ -12,19 +12,27 @@ import (
// Read a YAML configuration and create a Configuration object out of it. // Read a YAML configuration and create a Configuration object out of it.
func Read(configPath string) (*schema.Configuration, []error) { func Read(configPath string) (*schema.Configuration, []error) {
viper.SetEnvPrefix("AUTHELIA")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// we need to bind all env variables as long as https://github.com/spf13/viper/issues/761 // we need to bind all env variables as long as https://github.com/spf13/viper/issues/761
// is not resolved. // is not resolved.
viper.BindEnv("jwt_secret") //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("duo_api.secret_key") //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("session.secret") //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("authentication_backend.ldap.password") //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("notifier.smtp.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("session.redis.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("storage.mysql.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("storage.postgres.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) 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. viper.Unmarshal(&configuration) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
val := schema.NewStructValidator() val := schema.NewStructValidator()
validator.Validate(&configuration, val) validator.ValidateSecrets(&configuration, val, viper.GetViper())
validator.ValidateConfiguration(&configuration, val)
if val.HasErrors() { if val.HasErrors() {
return nil, val.Errors() return nil, val.Errors()

View File

@ -2,12 +2,25 @@ package configuration
import ( import (
"os" "os"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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) { func TestShouldParseConfigFile(t *testing.T) {
require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET", "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_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_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_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_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") 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, "smtp_secret_from_env", config.Notifier.SMTP.Password)
assert.Equal(t, "redis_secret_from_env", config.Session.Redis.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, "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, "postgres_secret_from_env", config.Storage.PostgreSQL.Password)
assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) assert.Equal(t, "deny", config.AccessControl.DefaultPolicy)
assert.Len(t, config.AccessControl.Rules, 12) 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")
}

View File

@ -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

View File

@ -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

View File

@ -10,8 +10,8 @@ import (
var defaultPort = 8080 var defaultPort = 8080
var defaultLogLevel = "info" var defaultLogLevel = "info"
// Validate and adapt the configuration read from file. // ValidateConfiguration and adapt the configuration read from file.
func Validate(configuration *schema.Configuration, validator *schema.StructValidator) { func ValidateConfiguration(configuration *schema.Configuration, validator *schema.StructValidator) {
if configuration.Host == "" { if configuration.Host == "" {
configuration.Host = "0.0.0.0" configuration.Host = "0.0.0.0"
} }

View File

@ -37,7 +37,7 @@ func TestShouldNotUpdateConfig(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultConfig() config := newDefaultConfig()
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, 9090, config.Port) assert.Equal(t, 9090, config.Port)
@ -49,7 +49,7 @@ func TestShouldValidateAndUpdatePort(t *testing.T) {
config := newDefaultConfig() config := newDefaultConfig()
config.Port = 0 config.Port = 0
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, 8080, config.Port) assert.Equal(t, 8080, config.Port)
@ -60,7 +60,7 @@ func TestShouldValidateAndUpdateHost(t *testing.T) {
config := newDefaultConfig() config := newDefaultConfig()
config.Host = "" config.Host = ""
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, "0.0.0.0", config.Host) assert.Equal(t, "0.0.0.0", config.Host)
@ -71,7 +71,7 @@ func TestShouldValidateAndUpdateLogsLevel(t *testing.T) {
config := newDefaultConfig() config := newDefaultConfig()
config.LogLevel = "" config.LogLevel = ""
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, "info", config.LogLevel) assert.Equal(t, "info", config.LogLevel)
@ -81,12 +81,12 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultConfig() config := newDefaultConfig()
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
config.Notifier = nil config.Notifier = nil
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "A notifier configuration must be provided") assert.EqualError(t, validator.Errors()[0], "A notifier configuration must be provided")
} }
@ -95,7 +95,7 @@ func TestShouldAddDefaultAccessControl(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultConfig() config := newDefaultConfig()
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.NotNil(t, config.AccessControl) assert.NotNil(t, config.AccessControl)
assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) assert.Equal(t, "deny", config.AccessControl.DefaultPolicy)
@ -106,7 +106,7 @@ func TestShouldRaiseErrorWhenTLSCertWithoutKeyIsProvided(t *testing.T) {
config := newDefaultConfig() config := newDefaultConfig()
config.TLSCert = "/tmp/cert.pem" config.TLSCert = "/tmp/cert.pem"
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) 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") 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 := newDefaultConfig()
config.TLSKey = "/tmp/key.pem" config.TLSKey = "/tmp/key.pem"
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) 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") 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.TLSCert = "/tmp/cert.pem"
config.TLSKey = "/tmp/key.pem" config.TLSKey = "/tmp/key.pem"
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
} }
@ -136,7 +136,7 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) {
config := newDefaultConfig() config := newDefaultConfig()
config.JWTSecret = "" config.JWTSecret = ""
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Provide a JWT secret using \"jwt_secret\" key") 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 := newDefaultConfig()
config.DefaultRedirectionURL = "abc" config.DefaultRedirectionURL = "abc"
Validate(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Unable to parse default redirection url") assert.EqualError(t, validator.Errors()[0], "Unable to parse default redirection url")
} }

View File

@ -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
}

View File

@ -40,7 +40,7 @@ func validatePostgreSQLConfiguration(configuration *schema.PostgreSQLStorageConf
if !(configuration.SSLMode == "disable" || configuration.SSLMode == "require" || if !(configuration.SSLMode == "disable" || configuration.SSLMode == "require" ||
configuration.SSLMode == "verify-ca" || configuration.SSLMode == "verify-full") { 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'"))
} }
} }

View File

@ -94,7 +94,7 @@ func (s *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() {
ValidateStorage(s.configuration, validator) ValidateStorage(s.configuration, validator)
s.Require().Len(validator.Errors(), 1) 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) { func TestShouldRunStorageSuite(t *testing.T) {