Skip to content

Commit

Permalink
[DEPRECATE] Environment Variable Secrets (#905)
Browse files Browse the repository at this point in the history
* remove ENV usages
* fix reader unit tests
* fix standalone suite
* fix k8s suite
* apply suggestions from code review

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
  • Loading branch information
james-d-elliott and nightah committed May 8, 2020
1 parent a70e460 commit 9e7947a
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 107 deletions.
5 changes: 5 additions & 0 deletions BREAKING.md
Expand Up @@ -6,6 +6,11 @@ recommended not to use the 'latest' Docker image tag blindly but pick a version
and read this documentation before upgrading. This is where you will get information about
breaking changes and about what you should do to overcome those changes.

## Breaking in v4.18.0
* Secrets stored directly in ENV are now removed from Authelia. They have been replaced with file
secrets. If you still have not moved feel free to contact the team for assistance, otherwise the
[documentation](https://docs.authelia.com/configuration/secrets.html) has instructions on how to utilize these.

## Breaking in v4.15.0
* Previously if a configuration value did not exist we ignored it. Now we will error if someone has
specified either an unknown configuration key or one that has changed. In the instance of a changed
Expand Down
10 changes: 5 additions & 5 deletions compose/lite/configuration.yml
Expand Up @@ -5,7 +5,7 @@
host: 0.0.0.0
port: 9091
log_level: debug
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
jwt_secret: a_very_important_secret
default_redirection_url: https://public.example.com
totp:
Expand All @@ -14,7 +14,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
# # This secret can also be set using the env variables AUTHELIA_DUO_API_SECRET_KEY_FILE
# secret_key: 1234567890abcdefghifjkl

authentication_backend:
Expand All @@ -34,7 +34,7 @@ access_control:

session:
name: authelia_session
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
secret: unsecure_session_secret
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
Expand All @@ -43,7 +43,7 @@ session:
redis:
host: redis
port: 6379
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
password: authelia

regulation:
Expand All @@ -58,7 +58,7 @@ storage:
notifier:
smtp:
username: test
# This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD
# This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
password: password
host: mail.example.com
port: 25
Expand Down
26 changes: 10 additions & 16 deletions docs/configuration/secrets.md
Expand Up @@ -42,29 +42,23 @@ environment variable will not be replaced.
|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

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.

## Secrets exposed in an environment variable

**DEPRECATION NOTICE:** This backwards compatibility feature **has been removed** in 4.18.0+.

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 no longer available
as an option**, please see the table above for the file based replacements. See
[this article](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/)
for reasons why this was removed.

## Docker

Expand Down
11 changes: 0 additions & 11 deletions internal/configuration/reader.go
Expand Up @@ -14,17 +14,6 @@ import (
func Read(configPath string) (*schema.Configuration, []error) {
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("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.
Expand Down
131 changes: 79 additions & 52 deletions internal/configuration/reader_test.go
@@ -1,36 +1,73 @@
package configuration

import (
"io/ioutil"
"os"
"path"
"sort"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/utils"
)

func createTestingTempFile(t *testing.T, dir, name, content string) {
err := ioutil.WriteFile(path.Join(dir, name), []byte(content), 0600)
require.NoError(t, err)
}

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")
_ = os.Unsetenv("AUTHELIA_JWT_SECRET_FILE")
_ = os.Unsetenv("AUTHELIA_DUO_API_SECRET_KEY_FILE")
_ = os.Unsetenv("AUTHELIA_SESSION_SECRET_FILE")
_ = os.Unsetenv("AUTHELIA_SESSION_SECRET_FILE")
_ = 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_STORAGE_MYSQL_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE")
}

func setupEnv(t *testing.T) string {
resetEnv()

dirEnv := os.Getenv("AUTHELIA_TESTING_DIR")
if dirEnv != "" {
return dirEnv
}

dir := "/tmp/authelia" + utils.RandomString(10, authentication.HashingPossibleSaltCharacters) + "/"
err := os.MkdirAll(dir, 0700)
require.NoError(t, err)

createTestingTempFile(t, dir, "jwt", "secret_from_env")
createTestingTempFile(t, dir, "duo", "duo_secret_from_env")
createTestingTempFile(t, dir, "session", "session_secret_from_env")
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, "mysql", "mysql_secret_from_env")
createTestingTempFile(t, dir, "postgres", "postgres_secret_from_env")

require.NoError(t, os.Setenv("AUTHELIA_TESTING_DIR", dir))

return dir
}

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"))
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"))
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"))
dir := setupEnv(t)

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_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres"))

config, errors := Read("./test_resources/config.yml")

Expand All @@ -57,7 +94,12 @@ func TestShouldParseConfigFile(t *testing.T) {
}

func TestShouldParseAltConfigFile(t *testing.T) {
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD", "postgres_secret_from_env"))
dir := setupEnv(t)

require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres"))
require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication"))
require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET_FILE", dir+"jwt"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_SECRET_FILE", dir+"session"))

config, errors := Read("./test_resources/config_alt.yml")
require.Len(t, errors, 0)
Expand All @@ -77,14 +119,15 @@ func TestShouldParseAltConfigFile(t *testing.T) {
}

func TestShouldNotParseConfigFileWithOldOrUnexpectedKeys(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"))
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"))
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"))
dir := setupEnv(t)

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_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))

_, errors := Read("./test_resources/config_bad_keys.yml")
require.Len(t, errors, 2)
Expand All @@ -104,33 +147,17 @@ func TestShouldValidateConfigurationTemplate(t *testing.T) {
assert.Len(t, errors, 0)
}

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) {
dir := setupEnv(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"))
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_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))

_, errors := Read("./test_resources/config_with_secret.yml")

Expand Down
13 changes: 1 addition & 12 deletions internal/configuration/validator/secrets.go
Expand Up @@ -8,7 +8,6 @@ import (
"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.
Expand Down Expand Up @@ -43,15 +42,10 @@ func ValidateSecrets(configuration *schema.Configuration, validator *schema.Stru

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 != "" {
if fileEnvValue != "" && configValue != "" {
validator.Push(fmt.Errorf("error loading secret (%s): it's already defined in the config file", name))
}

Expand All @@ -65,10 +59,5 @@ func getSecretValue(name string, validator *schema.StructValidator, viper *viper
}
}

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
}
4 changes: 2 additions & 2 deletions internal/suites/Standalone/docker-compose.yml
Expand Up @@ -2,8 +2,8 @@ version: '3'
services:
authelia-backend:
environment:
- AUTHELIA_JWT_SECRET=very_important_secret
- AUTHELIA_SESSION_SECRET=unsecure_session_secret
- AUTHELIA_JWT_SECRET_FILE=/tmp/authelia/StandaloneSuite/jwt
- AUTHELIA_SESSION_SECRET_FILE=/tmp/authelia/StandaloneSuite/session
volumes:
- './Standalone/configuration.yml:/etc/authelia/configuration.yml:ro'
- './Standalone/users.yml:/var/lib/authelia/users.yml'
Expand Down
32 changes: 23 additions & 9 deletions internal/suites/example/kube/authelia/deployment.yml
Expand Up @@ -26,17 +26,20 @@ spec:
mountPath: /etc/authelia
- name: ssl-volume
mountPath: /var/lib/authelia/ssl
- name: secrets
mountPath: /usr/app/secrets
readOnly: true
env:
# We set secrets directly here for ease of deployment but all secrets
# should be stored in the Kube Vault in production.
- name: AUTHELIA_JWT_SECRET
value: an_unsecure_secret
- name: AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD
value: password
- name: AUTHELIA_SESSION_SECRET
value: unsecure_password
- name: AUTHELIA_STORAGE_MYSQL_PASSWORD
value: password
- name: AUTHELIA_JWT_SECRET_FILE
value: /usr/app/secrets/jwt_secret
- name: AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE
value: /usr/app/secrets/ldap_password
- name: AUTHELIA_SESSION_SECRET_FILE
value: /usr/app/secrets/session
- name: AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE
value: /usr/app/secrets/sql_password
volumes:
- name: config-volume
configMap:
Expand All @@ -52,4 +55,15 @@ spec:
path: cert.pem
- key: key.pem
path: key.pem

- name: secrets
secret:
secretName: authelia
items:
- key: jwt_secret
path: jwt_secret
- key: session
path: session
- key: sql_password
path: sql_password
- key: ldap_password
path: ldap_password
14 changes: 14 additions & 0 deletions internal/suites/example/kube/authelia/secret.yml
@@ -0,0 +1,14 @@
---
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: authelia
namespace: authelia
labels:
app: authelia
data:
jwt_secret: YW5fdW5zZWN1cmVfc2VjcmV0 #an_unsecure_secret
ldap_password: cGFzc3dvcmQ= #password
session: dW5zZWN1cmVfcGFzc3dvcmQ= #unsecure_password
sql_password: cGFzc3dvcmQ= #password

0 comments on commit 9e7947a

Please sign in to comment.