From dc7ca6f03c97f22057b7a02cd0e71bb79926b4b9 Mon Sep 17 00:00:00 2001
From: James Elliott <james-d-elliott@users.noreply.github.com>
Date: Sat, 16 Apr 2022 19:00:39 +1000
Subject: [PATCH] refactor: introduce config key gen (#3206)

This adjusts the validated keys to utilize a generated code section.
---
 cmd/authelia-scripts/cmd_gen.go               | 199 ++++++++++++++
 cmd/authelia-scripts/main.go                  |   2 +-
 internal/configuration/koanf_callbacks.go     |   5 +-
 internal/configuration/schema/keys.go         | 196 ++++++++++++++
 internal/configuration/schema/keys_test.go    | 253 ++++++++++++++++++
 internal/configuration/sources.go             |   5 +-
 internal/configuration/validator/const.go     | 234 ----------------
 internal/configuration/validator/keys.go      |   2 +-
 internal/configuration/validator/keys_test.go |   6 +-
 9 files changed, 657 insertions(+), 245 deletions(-)
 create mode 100644 cmd/authelia-scripts/cmd_gen.go
 create mode 100644 internal/configuration/schema/keys.go
 create mode 100644 internal/configuration/schema/keys_test.go

diff --git a/cmd/authelia-scripts/cmd_gen.go b/cmd/authelia-scripts/cmd_gen.go
new file mode 100644
index 00000000..cc8bfa94
--- /dev/null
+++ b/cmd/authelia-scripts/cmd_gen.go
@@ -0,0 +1,199 @@
+package main
+
+import (
+	"fmt"
+	"net/mail"
+	"net/url"
+	"os"
+	"reflect"
+	"regexp"
+	"strings"
+	"text/template"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/authelia/authelia/v4/internal/configuration/schema"
+)
+
+// NewRunGenCmd implements the code generation cobra command.
+func NewRunGenCmd() (cmd *cobra.Command) {
+	cmd = &cobra.Command{
+		Use:  "gen",
+		RunE: runGenE,
+	}
+
+	return cmd
+}
+
+func runGenE(cmd *cobra.Command, args []string) (err error) {
+	if err = genConfigurationKeys(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func genConfigurationKeys() (err error) {
+	data := loadKeysTemplate()
+
+	f, err := os.Create("./internal/configuration/schema/keys.go")
+	if err != nil {
+		return err
+	}
+
+	return keysTemplate.Execute(f, data)
+}
+
+var keysTemplate = template.Must(template.New("keys").Parse(`// Code generated by go generate. DO NOT EDIT.
+//
+// Run the following command to generate this file:
+// 		go run ./cmd/authelia-scripts gen
+//
+
+package schema
+
+// Keys represents the detected schema keys.
+var Keys = []string{
+{{- range .Keys }}
+	{{ printf "%q" . }},
+{{- end }}
+}
+`))
+
+type keysTemplateStruct struct {
+	Timestamp time.Time
+	Keys      []string
+}
+
+func loadKeysTemplate() keysTemplateStruct {
+	config := schema.Configuration{
+		Storage: schema.StorageConfiguration{
+			Local:      &schema.LocalStorageConfiguration{},
+			MySQL:      &schema.MySQLStorageConfiguration{},
+			PostgreSQL: &schema.PostgreSQLStorageConfiguration{},
+		},
+		Notifier: schema.NotifierConfiguration{
+			FileSystem: &schema.FileSystemNotifierConfiguration{},
+			SMTP: &schema.SMTPNotifierConfiguration{
+				TLS: &schema.TLSConfig{},
+			},
+		},
+		AuthenticationBackend: schema.AuthenticationBackendConfiguration{
+			File: &schema.FileAuthenticationBackendConfiguration{
+				Password: &schema.PasswordConfiguration{},
+			},
+			LDAP: &schema.LDAPAuthenticationBackendConfiguration{
+				TLS: &schema.TLSConfig{},
+			},
+		},
+		Session: schema.SessionConfiguration{
+			Redis: &schema.RedisSessionConfiguration{
+				TLS:              &schema.TLSConfig{},
+				HighAvailability: &schema.RedisHighAvailabilityConfiguration{},
+			},
+		},
+		IdentityProviders: schema.IdentityProvidersConfiguration{
+			OIDC: &schema.OpenIDConnectConfiguration{},
+		},
+	}
+
+	return keysTemplateStruct{
+		Timestamp: time.Now(),
+		Keys:      readTags("", reflect.TypeOf(config)),
+	}
+}
+
+var decodedTypes = []reflect.Type{
+	reflect.TypeOf(mail.Address{}),
+	reflect.TypeOf(regexp.Regexp{}),
+	reflect.TypeOf(url.URL{}),
+	reflect.TypeOf(time.Duration(0)),
+}
+
+func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) {
+	for _, t := range haystack {
+		if needle.Kind() == reflect.Ptr {
+			if needle.Elem() == t {
+				return true
+			}
+		} else if needle == t {
+			return true
+		}
+	}
+
+	return false
+}
+
+func readTags(prefix string, t reflect.Type) (tags []string) {
+	tags = make([]string, 0)
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+
+		tag := field.Tag.Get("koanf")
+
+		if tag == "" {
+			tags = append(tags, prefix)
+
+			continue
+		}
+
+		switch field.Type.Kind() {
+		case reflect.Struct:
+			if !containsType(field.Type, decodedTypes) {
+				tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...)
+
+				continue
+			}
+		case reflect.Slice:
+			if field.Type.Elem().Kind() == reflect.Struct {
+				if !containsType(field.Type.Elem(), decodedTypes) {
+					tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false))
+					tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
+
+					continue
+				}
+			}
+		case reflect.Ptr:
+			switch field.Type.Elem().Kind() {
+			case reflect.Struct:
+				if !containsType(field.Type.Elem(), decodedTypes) {
+					tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...)
+
+					continue
+				}
+			case reflect.Slice:
+				if field.Type.Elem().Elem().Kind() == reflect.Struct {
+					if !containsType(field.Type.Elem(), decodedTypes) {
+						tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
+
+						continue
+					}
+				}
+			}
+		}
+
+		tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false))
+	}
+
+	return tags
+}
+
+func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string {
+	nameParts := strings.SplitN(name, ",", 2)
+
+	if prefix == "" {
+		return nameParts[0]
+	}
+
+	if len(nameParts) == 2 && nameParts[1] == "squash" {
+		return prefix
+	}
+
+	if slice {
+		return fmt.Sprintf("%s.%s[]", prefix, nameParts[0])
+	}
+
+	return fmt.Sprintf("%s.%s", prefix, nameParts[0])
+}
diff --git a/cmd/authelia-scripts/main.go b/cmd/authelia-scripts/main.go
index cd812d29..011cb09b 100755
--- a/cmd/authelia-scripts/main.go
+++ b/cmd/authelia-scripts/main.go
@@ -136,7 +136,7 @@ func main() {
 		cobraCommands = append(cobraCommands, command)
 	}
 
-	cobraCommands = append(cobraCommands, commands.NewHashPasswordCmd(), commands.NewCertificatesCmd(), commands.NewRSACmd(), xflagsCmd)
+	cobraCommands = append(cobraCommands, commands.NewHashPasswordCmd(), commands.NewCertificatesCmd(), commands.NewRSACmd(), NewRunGenCmd(), xflagsCmd)
 
 	rootCmd.PersistentFlags().BoolVar(&buildkite, "buildkite", false, "Set CI flag for Buildkite")
 	rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the log level for the command")
diff --git a/internal/configuration/koanf_callbacks.go b/internal/configuration/koanf_callbacks.go
index 384b87c8..26d11ebc 100644
--- a/internal/configuration/koanf_callbacks.go
+++ b/internal/configuration/koanf_callbacks.go
@@ -7,7 +7,6 @@ import (
 	"github.com/spf13/pflag"
 
 	"github.com/authelia/authelia/v4/internal/configuration/schema"
-	"github.com/authelia/authelia/v4/internal/configuration/validator"
 	"github.com/authelia/authelia/v4/internal/utils"
 )
 
@@ -25,7 +24,7 @@ func koanfEnvironmentCallback(keyMap map[string]string, ignoredKeys []string, pr
 		formattedKey := strings.TrimPrefix(key, prefix)
 		formattedKey = strings.ReplaceAll(strings.ToLower(formattedKey), delimiter, constDelimiter)
 
-		if utils.IsStringInSlice(formattedKey, validator.ValidKeys) {
+		if utils.IsStringInSlice(formattedKey, schema.Keys) {
 			return formattedKey, value
 		}
 
@@ -64,7 +63,7 @@ func koanfCommandLineWithMappingCallback(mapping map[string]string, includeValid
 		if includeValidKeys {
 			formattedKey := strings.ReplaceAll(flag.Name, "-", "_")
 
-			if utils.IsStringInSlice(formattedKey, validator.ValidKeys) {
+			if utils.IsStringInSlice(formattedKey, schema.Keys) {
 				return formattedKey, flag.Value.String()
 			}
 		}
diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go
new file mode 100644
index 00000000..6c3124e7
--- /dev/null
+++ b/internal/configuration/schema/keys.go
@@ -0,0 +1,196 @@
+// Code generated by go generate. DO NOT EDIT.
+//
+// Run the following command to generate this file:
+// 		go run ./cmd/authelia-scripts gen
+//
+
+package schema
+
+// Keys represents the detected schema keys.
+var Keys = []string{
+	"theme",
+	"certificates_directory",
+	"jwt_secret",
+	"default_redirection_url",
+	"log.level",
+	"log.format",
+	"log.file_path",
+	"log.keep_stdout",
+	"identity_providers.oidc.hmac_secret",
+	"identity_providers.oidc.issuer_private_key",
+	"identity_providers.oidc.access_token_lifespan",
+	"identity_providers.oidc.authorize_code_lifespan",
+	"identity_providers.oidc.id_token_lifespan",
+	"identity_providers.oidc.refresh_token_lifespan",
+	"identity_providers.oidc.enable_client_debug_messages",
+	"identity_providers.oidc.minimum_parameter_entropy",
+	"identity_providers.oidc.enforce_pkce",
+	"identity_providers.oidc.enable_pkce_plain_challenge",
+	"identity_providers.oidc.cors.endpoints",
+	"identity_providers.oidc.cors.allowed_origins",
+	"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris",
+	"identity_providers.oidc.clients",
+	"identity_providers.oidc.clients[].id",
+	"identity_providers.oidc.clients[].description",
+	"identity_providers.oidc.clients[].secret",
+	"identity_providers.oidc.clients[].sector_identifier",
+	"identity_providers.oidc.clients[].public",
+	"identity_providers.oidc.clients[].redirect_uris",
+	"identity_providers.oidc.clients[].audience",
+	"identity_providers.oidc.clients[].scopes",
+	"identity_providers.oidc.clients[].grant_types",
+	"identity_providers.oidc.clients[].response_types",
+	"identity_providers.oidc.clients[].response_modes",
+	"identity_providers.oidc.clients[].userinfo_signing_algorithm",
+	"identity_providers.oidc.clients[].authorization_policy",
+	"identity_providers.oidc.clients[].pre_configured_consent_duration",
+	"authentication_backend.ldap.implementation",
+	"authentication_backend.ldap.url",
+	"authentication_backend.ldap.timeout",
+	"authentication_backend.ldap.start_tls",
+	"authentication_backend.ldap.tls.minimum_version",
+	"authentication_backend.ldap.tls.skip_verify",
+	"authentication_backend.ldap.tls.server_name",
+	"authentication_backend.ldap.base_dn",
+	"authentication_backend.ldap.additional_users_dn",
+	"authentication_backend.ldap.users_filter",
+	"authentication_backend.ldap.additional_groups_dn",
+	"authentication_backend.ldap.groups_filter",
+	"authentication_backend.ldap.group_name_attribute",
+	"authentication_backend.ldap.username_attribute",
+	"authentication_backend.ldap.mail_attribute",
+	"authentication_backend.ldap.display_name_attribute",
+	"authentication_backend.ldap.user",
+	"authentication_backend.ldap.password",
+	"authentication_backend.file.path",
+	"authentication_backend.file.password.iterations",
+	"authentication_backend.file.password.key_length",
+	"authentication_backend.file.password.salt_length",
+	"authentication_backend.file.password.algorithm",
+	"authentication_backend.file.password.memory",
+	"authentication_backend.file.password.parallelism",
+	"authentication_backend.password_reset.custom_url",
+	"authentication_backend.disable_reset_password",
+	"authentication_backend.refresh_interval",
+	"session.name",
+	"session.domain",
+	"session.same_site",
+	"session.secret",
+	"session.expiration",
+	"session.inactivity",
+	"session.remember_me_duration",
+	"session.redis.host",
+	"session.redis.port",
+	"session.redis.username",
+	"session.redis.password",
+	"session.redis.database_index",
+	"session.redis.maximum_active_connections",
+	"session.redis.minimum_idle_connections",
+	"session.redis.tls.minimum_version",
+	"session.redis.tls.skip_verify",
+	"session.redis.tls.server_name",
+	"session.redis.high_availability.sentinel_name",
+	"session.redis.high_availability.sentinel_username",
+	"session.redis.high_availability.sentinel_password",
+	"session.redis.high_availability.nodes",
+	"session.redis.high_availability.nodes[].host",
+	"session.redis.high_availability.nodes[].port",
+	"session.redis.high_availability.route_by_latency",
+	"session.redis.high_availability.route_randomly",
+	"totp.disable",
+	"totp.issuer",
+	"totp.algorithm",
+	"totp.digits",
+	"totp.period",
+	"totp.skew",
+	"totp.secret_size",
+	"duo_api.disable",
+	"duo_api.hostname",
+	"duo_api.integration_key",
+	"duo_api.secret_key",
+	"duo_api.enable_self_enrollment",
+	"access_control.default_policy",
+	"access_control.networks",
+	"access_control.networks[].name",
+	"access_control.networks[].networks",
+	"access_control.rules",
+	"access_control.rules[].domain",
+	"access_control.rules[].domain_regex",
+	"access_control.rules[].policy",
+	"access_control.rules[].subject",
+	"access_control.rules[].networks",
+	"access_control.rules[].resources",
+	"access_control.rules[].methods",
+	"ntp.address",
+	"ntp.version",
+	"ntp.max_desync",
+	"ntp.disable_startup_check",
+	"ntp.disable_failure",
+	"regulation.max_retries",
+	"regulation.find_time",
+	"regulation.ban_time",
+	"storage.local.path",
+	"storage.mysql.host",
+	"storage.mysql.port",
+	"storage.mysql.database",
+	"storage.mysql.username",
+	"storage.mysql.password",
+	"storage.mysql.timeout",
+	"storage.postgres.host",
+	"storage.postgres.port",
+	"storage.postgres.database",
+	"storage.postgres.username",
+	"storage.postgres.password",
+	"storage.postgres.timeout",
+	"storage.postgres.schema",
+	"storage.postgres.ssl.mode",
+	"storage.postgres.ssl.root_certificate",
+	"storage.postgres.ssl.certificate",
+	"storage.postgres.ssl.key",
+	"storage.postgres.sslmode",
+	"storage.encryption_key",
+	"notifier.disable_startup_check",
+	"notifier.filesystem.filename",
+	"notifier.smtp.host",
+	"notifier.smtp.port",
+	"notifier.smtp.timeout",
+	"notifier.smtp.username",
+	"notifier.smtp.password",
+	"notifier.smtp.identifier",
+	"notifier.smtp.sender",
+	"notifier.smtp.subject",
+	"notifier.smtp.startup_check_address",
+	"notifier.smtp.disable_require_tls",
+	"notifier.smtp.disable_html_emails",
+	"notifier.smtp.tls.minimum_version",
+	"notifier.smtp.tls.skip_verify",
+	"notifier.smtp.tls.server_name",
+	"notifier.template_path",
+	"server.host",
+	"server.port",
+	"server.path",
+	"server.asset_path",
+	"server.read_buffer_size",
+	"server.write_buffer_size",
+	"server.enable_pprof",
+	"server.enable_expvars",
+	"server.disable_healthcheck",
+	"server.tls.certificate",
+	"server.tls.key",
+	"server.tls.client_certificates",
+	"server.headers.csp_template",
+	"webauthn.disable",
+	"webauthn.display_name",
+	"webauthn.attestation_conveyance_preference",
+	"webauthn.user_verification",
+	"webauthn.timeout",
+	"password_policy.standard.enabled",
+	"password_policy.standard.min_length",
+	"password_policy.standard.max_length",
+	"password_policy.standard.require_uppercase",
+	"password_policy.standard.require_lowercase",
+	"password_policy.standard.require_number",
+	"password_policy.standard.require_special",
+	"password_policy.zxcvbn.enabled",
+	"password_policy.zxcvbn.min_score",
+}
diff --git a/internal/configuration/schema/keys_test.go b/internal/configuration/schema/keys_test.go
new file mode 100644
index 00000000..13990c80
--- /dev/null
+++ b/internal/configuration/schema/keys_test.go
@@ -0,0 +1,253 @@
+package schema
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var ValidKeys = []string{
+	// Root Keys.
+	"certificates_directory",
+	"theme",
+	"default_redirection_url",
+	"jwt_secret",
+
+	// Log keys.
+	"log.level",
+	"log.format",
+	"log.file_path",
+	"log.keep_stdout",
+
+	// Server Keys.
+	"server.host",
+	"server.port",
+	"server.read_buffer_size",
+	"server.write_buffer_size",
+	"server.path",
+	"server.asset_path",
+	"server.enable_pprof",
+	"server.enable_expvars",
+	"server.disable_healthcheck",
+	"server.tls.key",
+	"server.tls.certificate",
+	"server.tls.client_certificates",
+	"server.headers.csp_template",
+
+	// TOTP Keys.
+	"totp.disable",
+	"totp.issuer",
+	"totp.algorithm",
+	"totp.digits",
+	"totp.period",
+	"totp.skew",
+	"totp.secret_size",
+
+	// Webauthn Keys.
+	"webauthn.disable",
+	"webauthn.display_name",
+	"webauthn.attestation_conveyance_preference",
+	"webauthn.user_verification",
+	"webauthn.timeout",
+
+	// DUO API Keys.
+	"duo_api.disable",
+	"duo_api.hostname",
+	"duo_api.enable_self_enrollment",
+	"duo_api.secret_key",
+	"duo_api.integration_key",
+
+	// Access Control Keys.
+	"access_control.default_policy",
+	"access_control.networks",
+	"access_control.networks[].name",
+	"access_control.networks[].networks",
+	"access_control.rules",
+	"access_control.rules[].domain",
+	"access_control.rules[].domain_regex",
+	"access_control.rules[].methods",
+	"access_control.rules[].networks",
+	"access_control.rules[].subject",
+	"access_control.rules[].policy",
+	"access_control.rules[].resources",
+
+	// Session Keys.
+	"session.name",
+	"session.domain",
+	"session.secret",
+	"session.same_site",
+	"session.expiration",
+	"session.inactivity",
+	"session.remember_me_duration",
+
+	// Redis Session Keys.
+	"session.redis.host",
+	"session.redis.port",
+	"session.redis.username",
+	"session.redis.password",
+	"session.redis.database_index",
+	"session.redis.maximum_active_connections",
+	"session.redis.minimum_idle_connections",
+	"session.redis.tls.minimum_version",
+	"session.redis.tls.skip_verify",
+	"session.redis.tls.server_name",
+	"session.redis.high_availability.sentinel_name",
+	"session.redis.high_availability.sentinel_username",
+	"session.redis.high_availability.sentinel_password",
+	"session.redis.high_availability.nodes",
+	"session.redis.high_availability.nodes[].host",
+	"session.redis.high_availability.nodes[].port",
+	"session.redis.high_availability.route_by_latency",
+	"session.redis.high_availability.route_randomly",
+
+	// Storage Keys.
+	"storage.encryption_key",
+
+	// Local Storage Keys.
+	"storage.local.path",
+
+	// MySQL Storage Keys.
+	"storage.mysql.host",
+	"storage.mysql.port",
+	"storage.mysql.database",
+	"storage.mysql.username",
+	"storage.mysql.password",
+	"storage.mysql.timeout",
+
+	// PostgreSQL Storage Keys.
+	"storage.postgres.host",
+	"storage.postgres.port",
+	"storage.postgres.database",
+	"storage.postgres.username",
+	"storage.postgres.password",
+	"storage.postgres.timeout",
+	"storage.postgres.schema",
+	"storage.postgres.ssl.mode",
+	"storage.postgres.ssl.root_certificate",
+	"storage.postgres.ssl.certificate",
+	"storage.postgres.ssl.key",
+
+	"storage.postgres.sslmode", // Deprecated. TODO: Remove in v4.36.0.
+
+	// FileSystem Notifier Keys.
+	"notifier.filesystem.filename",
+	"notifier.disable_startup_check",
+
+	// SMTP Notifier Keys.
+	"notifier.smtp.host",
+	"notifier.smtp.port",
+	"notifier.smtp.timeout",
+	"notifier.smtp.username",
+	"notifier.smtp.password",
+	"notifier.smtp.identifier",
+	"notifier.smtp.sender",
+	"notifier.smtp.subject",
+	"notifier.smtp.startup_check_address",
+	"notifier.smtp.disable_require_tls",
+	"notifier.smtp.disable_html_emails",
+	"notifier.smtp.tls.minimum_version",
+	"notifier.smtp.tls.skip_verify",
+	"notifier.smtp.tls.server_name",
+	"notifier.template_path",
+
+	// Regulation Keys.
+	"regulation.max_retries",
+	"regulation.find_time",
+	"regulation.ban_time",
+
+	// Authentication Backend Keys.
+	"authentication_backend.disable_reset_password",
+	"authentication_backend.password_reset.custom_url",
+	"authentication_backend.refresh_interval",
+
+	// LDAP Authentication Backend Keys.
+	"authentication_backend.ldap.implementation",
+	"authentication_backend.ldap.url",
+	"authentication_backend.ldap.timeout",
+	"authentication_backend.ldap.base_dn",
+	"authentication_backend.ldap.username_attribute",
+	"authentication_backend.ldap.additional_users_dn",
+	"authentication_backend.ldap.users_filter",
+	"authentication_backend.ldap.additional_groups_dn",
+	"authentication_backend.ldap.groups_filter",
+	"authentication_backend.ldap.group_name_attribute",
+	"authentication_backend.ldap.mail_attribute",
+	"authentication_backend.ldap.display_name_attribute",
+	"authentication_backend.ldap.user",
+	"authentication_backend.ldap.password",
+	"authentication_backend.ldap.start_tls",
+	"authentication_backend.ldap.tls.minimum_version",
+	"authentication_backend.ldap.tls.skip_verify",
+	"authentication_backend.ldap.tls.server_name",
+
+	// File Authentication Backend Keys.
+	"authentication_backend.file.path",
+	"authentication_backend.file.password.algorithm",
+	"authentication_backend.file.password.iterations",
+	"authentication_backend.file.password.key_length",
+	"authentication_backend.file.password.salt_length",
+	"authentication_backend.file.password.memory",
+	"authentication_backend.file.password.parallelism",
+
+	// Identity Provider Keys.
+	"identity_providers.oidc.hmac_secret",
+	"identity_providers.oidc.issuer_private_key",
+	"identity_providers.oidc.id_token_lifespan",
+	"identity_providers.oidc.access_token_lifespan",
+	"identity_providers.oidc.refresh_token_lifespan",
+	"identity_providers.oidc.authorize_code_lifespan",
+	"identity_providers.oidc.enforce_pkce",
+	"identity_providers.oidc.enable_pkce_plain_challenge",
+	"identity_providers.oidc.enable_client_debug_messages",
+	"identity_providers.oidc.minimum_parameter_entropy",
+	"identity_providers.oidc.cors.endpoints",
+	"identity_providers.oidc.cors.allowed_origins",
+	"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris",
+	"identity_providers.oidc.clients",
+	"identity_providers.oidc.clients[].id",
+	"identity_providers.oidc.clients[].description",
+	"identity_providers.oidc.clients[].secret",
+	"identity_providers.oidc.clients[].sector_identifier",
+	"identity_providers.oidc.clients[].public",
+	"identity_providers.oidc.clients[].redirect_uris",
+	"identity_providers.oidc.clients[].authorization_policy",
+	"identity_providers.oidc.clients[].pre_configured_consent_duration",
+	"identity_providers.oidc.clients[].scopes",
+	"identity_providers.oidc.clients[].audience",
+	"identity_providers.oidc.clients[].grant_types",
+	"identity_providers.oidc.clients[].response_types",
+	"identity_providers.oidc.clients[].response_modes",
+	"identity_providers.oidc.clients[].userinfo_signing_algorithm",
+
+	// NTP keys.
+	"ntp.address",
+	"ntp.version",
+	"ntp.max_desync",
+	"ntp.disable_startup_check",
+	"ntp.disable_failure",
+
+	// Password Policy keys.
+	"password_policy.standard.enabled",
+	"password_policy.standard.min_length",
+	"password_policy.standard.max_length",
+	"password_policy.standard.require_uppercase",
+	"password_policy.standard.require_lowercase",
+	"password_policy.standard.require_number",
+	"password_policy.standard.require_special",
+	"password_policy.zxcvbn.enabled",
+	"password_policy.zxcvbn.min_score",
+}
+
+func TestOldKeys(t *testing.T) {
+	for _, key := range ValidKeys {
+		assert.Contains(t, Keys, key)
+	}
+
+	for _, key := range Keys {
+		assert.Contains(t, ValidKeys, key)
+	}
+}
+
+func TestDuplicates(t *testing.T) {
+	assert.Equal(t, len(Keys), len(ValidKeys))
+}
diff --git a/internal/configuration/sources.go b/internal/configuration/sources.go
index 7506b2f0..22876e5b 100644
--- a/internal/configuration/sources.go
+++ b/internal/configuration/sources.go
@@ -12,7 +12,6 @@ import (
 	"github.com/spf13/pflag"
 
 	"github.com/authelia/authelia/v4/internal/configuration/schema"
-	"github.com/authelia/authelia/v4/internal/configuration/validator"
 )
 
 // NewYAMLFileSource returns a Source configured to load from a specified YAML path. If there is an issue accessing this
@@ -75,7 +74,7 @@ func (s *EnvironmentSource) Merge(ko *koanf.Koanf, _ *schema.StructValidator) (e
 
 // Load the Source into the EnvironmentSource koanf.Koanf.
 func (s *EnvironmentSource) Load(_ *schema.StructValidator) (err error) {
-	keyMap, ignoredKeys := getEnvConfigMap(validator.ValidKeys, s.prefix, s.delimiter)
+	keyMap, ignoredKeys := getEnvConfigMap(schema.Keys, s.prefix, s.delimiter)
 
 	return s.koanf.Load(env.ProviderWithValue(s.prefix, constDelimiter, koanfEnvironmentCallback(keyMap, ignoredKeys, s.prefix, s.delimiter)), nil)
 }
@@ -109,7 +108,7 @@ func (s *SecretsSource) Merge(ko *koanf.Koanf, val *schema.StructValidator) (err
 
 // Load the Source into the SecretsSource koanf.Koanf.
 func (s *SecretsSource) Load(val *schema.StructValidator) (err error) {
-	keyMap := getSecretConfigMap(validator.ValidKeys, s.prefix, s.delimiter)
+	keyMap := getSecretConfigMap(schema.Keys, s.prefix, s.delimiter)
 
 	return s.koanf.Load(env.ProviderWithValue(s.prefix, constDelimiter, koanfEnvironmentSecretsCallback(keyMap, val)), nil)
 }
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index b8465222..47484abb 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -300,240 +300,6 @@ var validOIDCCORSEndpoints = []string{oidc.AuthorizationEndpoint, oidc.TokenEndp
 
 var reKeyReplacer = regexp.MustCompile(`\[\d+]`)
 
-// ValidKeys is a list of valid keys that are not secret names. For the sake of consistency please place any secret in
-// the secret names map and reuse it in relevant sections.
-var ValidKeys = []string{
-	// Root Keys.
-	"certificates_directory",
-	"theme",
-	"default_redirection_url",
-	"jwt_secret",
-
-	// Log keys.
-	"log.level",
-	"log.format",
-	"log.file_path",
-	"log.keep_stdout",
-
-	// Server Keys.
-	"server.host",
-	"server.port",
-	"server.read_buffer_size",
-	"server.write_buffer_size",
-	"server.path",
-	"server.asset_path",
-	"server.enable_pprof",
-	"server.enable_expvars",
-	"server.disable_healthcheck",
-	"server.tls.key",
-	"server.tls.certificate",
-	"server.tls.client_certificates",
-	"server.headers.csp_template",
-
-	// TOTP Keys.
-	"totp.disable",
-	"totp.issuer",
-	"totp.algorithm",
-	"totp.digits",
-	"totp.period",
-	"totp.skew",
-	"totp.secret_size",
-
-	// Webauthn Keys.
-	"webauthn.disable",
-	"webauthn.display_name",
-	"webauthn.attestation_conveyance_preference",
-	"webauthn.user_verification",
-	"webauthn.timeout",
-
-	// DUO API Keys.
-	"duo_api.disable",
-	"duo_api.hostname",
-	"duo_api.enable_self_enrollment",
-	"duo_api.secret_key",
-	"duo_api.integration_key",
-
-	// Access Control Keys.
-	"access_control.default_policy",
-	"access_control.networks",
-	"access_control.networks[].name",
-	"access_control.networks[].networks",
-	"access_control.rules",
-	"access_control.rules[].domain",
-	"access_control.rules[].domain_regex",
-	"access_control.rules[].methods",
-	"access_control.rules[].networks",
-	"access_control.rules[].subject",
-	"access_control.rules[].policy",
-	"access_control.rules[].resources",
-
-	// Session Keys.
-	"session.name",
-	"session.domain",
-	"session.secret",
-	"session.same_site",
-	"session.expiration",
-	"session.inactivity",
-	"session.remember_me_duration",
-
-	// Redis Session Keys.
-	"session.redis.host",
-	"session.redis.port",
-	"session.redis.username",
-	"session.redis.password",
-	"session.redis.database_index",
-	"session.redis.maximum_active_connections",
-	"session.redis.minimum_idle_connections",
-	"session.redis.tls.minimum_version",
-	"session.redis.tls.skip_verify",
-	"session.redis.tls.server_name",
-	"session.redis.high_availability.sentinel_name",
-	"session.redis.high_availability.sentinel_username",
-	"session.redis.high_availability.sentinel_password",
-	"session.redis.high_availability.nodes",
-	"session.redis.high_availability.nodes[].host",
-	"session.redis.high_availability.nodes[].port",
-	"session.redis.high_availability.route_by_latency",
-	"session.redis.high_availability.route_randomly",
-
-	// Storage Keys.
-	"storage.encryption_key",
-
-	// Local Storage Keys.
-	"storage.local.path",
-
-	// MySQL Storage Keys.
-	"storage.mysql.host",
-	"storage.mysql.port",
-	"storage.mysql.database",
-	"storage.mysql.username",
-	"storage.mysql.password",
-	"storage.mysql.timeout",
-
-	// PostgreSQL Storage Keys.
-	"storage.postgres.host",
-	"storage.postgres.port",
-	"storage.postgres.database",
-	"storage.postgres.username",
-	"storage.postgres.password",
-	"storage.postgres.timeout",
-	"storage.postgres.schema",
-	"storage.postgres.ssl.mode",
-	"storage.postgres.ssl.root_certificate",
-	"storage.postgres.ssl.certificate",
-	"storage.postgres.ssl.key",
-
-	"storage.postgres.sslmode", // Deprecated. TODO: Remove in v4.36.0.
-
-	// FileSystem Notifier Keys.
-	"notifier.filesystem.filename",
-	"notifier.disable_startup_check",
-
-	// SMTP Notifier Keys.
-	"notifier.smtp.host",
-	"notifier.smtp.port",
-	"notifier.smtp.timeout",
-	"notifier.smtp.username",
-	"notifier.smtp.password",
-	"notifier.smtp.identifier",
-	"notifier.smtp.sender",
-	"notifier.smtp.subject",
-	"notifier.smtp.startup_check_address",
-	"notifier.smtp.disable_require_tls",
-	"notifier.smtp.disable_html_emails",
-	"notifier.smtp.tls.minimum_version",
-	"notifier.smtp.tls.skip_verify",
-	"notifier.smtp.tls.server_name",
-	"notifier.template_path",
-
-	// Regulation Keys.
-	"regulation.max_retries",
-	"regulation.find_time",
-	"regulation.ban_time",
-
-	// Authentication Backend Keys.
-	"authentication_backend.disable_reset_password",
-	"authentication_backend.password_reset.custom_url",
-	"authentication_backend.refresh_interval",
-
-	// LDAP Authentication Backend Keys.
-	"authentication_backend.ldap.implementation",
-	"authentication_backend.ldap.url",
-	"authentication_backend.ldap.timeout",
-	"authentication_backend.ldap.base_dn",
-	"authentication_backend.ldap.username_attribute",
-	"authentication_backend.ldap.additional_users_dn",
-	"authentication_backend.ldap.users_filter",
-	"authentication_backend.ldap.additional_groups_dn",
-	"authentication_backend.ldap.groups_filter",
-	"authentication_backend.ldap.group_name_attribute",
-	"authentication_backend.ldap.mail_attribute",
-	"authentication_backend.ldap.display_name_attribute",
-	"authentication_backend.ldap.user",
-	"authentication_backend.ldap.password",
-	"authentication_backend.ldap.start_tls",
-	"authentication_backend.ldap.tls.minimum_version",
-	"authentication_backend.ldap.tls.skip_verify",
-	"authentication_backend.ldap.tls.server_name",
-
-	// File Authentication Backend Keys.
-	"authentication_backend.file.path",
-	"authentication_backend.file.password.algorithm",
-	"authentication_backend.file.password.iterations",
-	"authentication_backend.file.password.key_length",
-	"authentication_backend.file.password.salt_length",
-	"authentication_backend.file.password.memory",
-	"authentication_backend.file.password.parallelism",
-
-	// Identity Provider Keys.
-	"identity_providers.oidc.hmac_secret",
-	"identity_providers.oidc.issuer_private_key",
-	"identity_providers.oidc.id_token_lifespan",
-	"identity_providers.oidc.access_token_lifespan",
-	"identity_providers.oidc.refresh_token_lifespan",
-	"identity_providers.oidc.authorize_code_lifespan",
-	"identity_providers.oidc.enforce_pkce",
-	"identity_providers.oidc.enable_pkce_plain_challenge",
-	"identity_providers.oidc.enable_client_debug_messages",
-	"identity_providers.oidc.minimum_parameter_entropy",
-	"identity_providers.oidc.cors.endpoints",
-	"identity_providers.oidc.cors.allowed_origins",
-	"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris",
-	"identity_providers.oidc.clients",
-	"identity_providers.oidc.clients[].id",
-	"identity_providers.oidc.clients[].description",
-	"identity_providers.oidc.clients[].secret",
-	"identity_providers.oidc.clients[].sector_identifier",
-	"identity_providers.oidc.clients[].public",
-	"identity_providers.oidc.clients[].redirect_uris",
-	"identity_providers.oidc.clients[].authorization_policy",
-	"identity_providers.oidc.clients[].pre_configured_consent_duration",
-	"identity_providers.oidc.clients[].scopes",
-	"identity_providers.oidc.clients[].audience",
-	"identity_providers.oidc.clients[].grant_types",
-	"identity_providers.oidc.clients[].response_types",
-	"identity_providers.oidc.clients[].response_modes",
-	"identity_providers.oidc.clients[].userinfo_signing_algorithm",
-
-	// NTP keys.
-	"ntp.address",
-	"ntp.version",
-	"ntp.max_desync",
-	"ntp.disable_startup_check",
-	"ntp.disable_failure",
-
-	// Password Policy keys.
-	"password_policy.standard.enabled",
-	"password_policy.standard.min_length",
-	"password_policy.standard.max_length",
-	"password_policy.standard.require_uppercase",
-	"password_policy.standard.require_lowercase",
-	"password_policy.standard.require_number",
-	"password_policy.standard.require_special",
-	"password_policy.zxcvbn.enabled",
-	"password_policy.zxcvbn.min_score",
-}
-
 var replacedKeys = map[string]string{
 	"authentication_backend.ldap.skip_verify":         "authentication_backend.ldap.tls.skip_verify",
 	"authentication_backend.ldap.minimum_tls_version": "authentication_backend.ldap.tls.minimum_version",
diff --git a/internal/configuration/validator/keys.go b/internal/configuration/validator/keys.go
index 4291447f..e3e5fdcf 100644
--- a/internal/configuration/validator/keys.go
+++ b/internal/configuration/validator/keys.go
@@ -16,7 +16,7 @@ func ValidateKeys(keys []string, prefix string, validator *schema.StructValidato
 	for _, key := range keys {
 		expectedKey := reKeyReplacer.ReplaceAllString(key, "[]")
 
-		if utils.IsStringInSlice(expectedKey, ValidKeys) {
+		if utils.IsStringInSlice(expectedKey, schema.Keys) {
 			continue
 		}
 
diff --git a/internal/configuration/validator/keys_test.go b/internal/configuration/validator/keys_test.go
index 23153666..989b4b81 100644
--- a/internal/configuration/validator/keys_test.go
+++ b/internal/configuration/validator/keys_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestShouldValidateGoodKeys(t *testing.T) {
-	configKeys := ValidKeys
+	configKeys := schema.Keys
 	val := schema.NewStructValidator()
 	ValidateKeys(configKeys, "AUTHELIA_", val)
 
@@ -20,7 +20,7 @@ func TestShouldValidateGoodKeys(t *testing.T) {
 }
 
 func TestShouldNotValidateBadKeys(t *testing.T) {
-	configKeys := ValidKeys
+	configKeys := schema.Keys
 	configKeys = append(configKeys, "bad_key")
 	configKeys = append(configKeys, "totp.skewy")
 	val := schema.NewStructValidator()
@@ -34,7 +34,7 @@ func TestShouldNotValidateBadKeys(t *testing.T) {
 }
 
 func TestShouldNotValidateBadEnvKeys(t *testing.T) {
-	configKeys := ValidKeys
+	configKeys := schema.Keys
 	configKeys = append(configKeys, "AUTHELIA__BAD_ENV_KEY")
 	configKeys = append(configKeys, "AUTHELIA_BAD_ENV_KEY")