[DEPRECATE] Environment Variable Secrets (#905)

* 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>
This commit is contained in:
James Elliott 2020-05-08 11:01:57 +10:00 committed by GitHub
parent a70e460ff4
commit 9e7947a193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 145 additions and 107 deletions

View File

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

View File

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

View File

@ -42,22 +42,6 @@ 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
@ -65,6 +49,16 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,12 +2,18 @@ package suites
import (
"fmt"
"io/ioutil"
"os"
"time"
)
var standaloneSuiteName = "Standalone"
func init() {
_ = os.MkdirAll("/tmp/authelia/StandaloneSuite/", 0700)
_ = ioutil.WriteFile("/tmp/authelia/StandaloneSuite/jwt", []byte("very_important_secret"), 0600) //nolint:gosec
_ = ioutil.WriteFile("/tmp/authelia/StandaloneSuite/session", []byte("unsecure_session_secret"), 0600) //nolint:gosec
dockerEnvironment := NewDockerEnvironment([]string{
"internal/suites/docker-compose.yml",
"internal/suites/Standalone/docker-compose.yml",