diff --git a/config.template.yml b/config.template.yml
index b349511f..e21d4291 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -108,6 +108,29 @@ duo_api:
   ## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
   secret_key: 1234567890abcdefghifjkl
 
+##
+## NTP Configuration
+##
+## This is used to validate the servers time is accurate enough to validate TOTP.
+ntp:
+  ## NTP server address.
+  address: "time.cloudflare.com:123"
+
+  ## NTP version.
+  version: 4
+
+  ## Maximum allowed time offset between the host and the NTP server.
+  max_desync: 3s
+
+  ## Disables the NTP check on startup entirely. This means Authelia will not contact a remote service at all if you
+  ## set this to true, and can operate in a truly offline mode.
+  disable_startup_check: false
+
+  ## The default of false will prevent startup only if we can contact the NTP server and the time is out of sync with
+  ## the NTP server more than the configured max_desync. If you set this to true, an error will be logged but startup
+  ## will continue regardless of results.
+  disable_failure: false
+
 ##
 ## Authentication Backend Provider Configuration
 ##
diff --git a/docs/configuration/ntp.md b/docs/configuration/ntp.md
new file mode 100644
index 00000000..20fbe34f
--- /dev/null
+++ b/docs/configuration/ntp.md
@@ -0,0 +1,91 @@
+---
+layout: default
+title: NTP
+parent: Configuration
+nav_order: 9
+---
+
+# NTP
+
+Authelia has the ability to check the system time against an NTP server. Currently this only occurs at startup. This
+section configures and tunes the settings for this check which is primarily used to ensure [TOTP](./one-time-password.md)
+can be accurately validated.
+
+In the instance of inability to contact the NTP server Authelia will just log an error and will continue to run.
+
+## Configuration
+
+```yaml
+ntp:
+  address: "time.cloudflare.com:123"
+  version: 3
+  max_desync: 3s
+  disable_startup_check: false
+  disable_failure: false
+```
+
+## Options
+
+### address
+<div markdown="1">
+type: string
+{: .label .label-config .label-purple } 
+default: time.cloudflare.com:123
+{: .label .label-config .label-blue }
+required: no
+{: .label .label-config .label-green }
+</div>
+
+Determines the address of the NTP server to retrieve the time from. The format is `<host>:<port>`, and both of these are
+required.
+
+### version
+<div markdown="1">
+type: integer
+{: .label .label-config .label-purple } 
+default: 4
+{: .label .label-config .label-blue }
+required: no
+{: .label .label-config .label-green }
+</div>
+
+Determines the NTP verion supported. Valid values are 3 or 4.
+
+### max_desync
+<div markdown="1">
+type: duration
+{: .label .label-config .label-purple } 
+default: 3s
+{: .label .label-config .label-blue }
+required: no
+{: .label .label-config .label-green }
+</div>
+
+This is used to tune the acceptable desync from the time reported from the NTP server. This uses our 
+[duration notation](./index.md#duration-notation-format) format.
+
+### disable_startup_check
+<div markdown="1">
+type: boolean
+{: .label .label-config .label-purple } 
+default: false
+{: .label .label-config .label-blue }
+required: no
+{: .label .label-config .label-green }
+</div>
+
+Setting this to true will disable the startup check entirely.
+
+### disable_failure
+<div markdown="1">
+type: boolean
+{: .label .label-config .label-purple } 
+default: false
+{: .label .label-config .label-blue }
+required: no
+{: .label .label-config .label-green }
+</div>
+
+Setting this to true will allow Authelia to start and just log an error instead of exiting. The default is that if
+Authelia can contact the NTP server successfully, and the time reported by the server is greater than what is configured
+in [max_desync](#max_desync) that Authelia fails to start and logs a fatal error.
\ No newline at end of file
diff --git a/docs/configuration/one-time-password.md b/docs/configuration/one-time-password.md
index 67f367bc..e6ff8c40 100644
--- a/docs/configuration/one-time-password.md
+++ b/docs/configuration/one-time-password.md
@@ -2,7 +2,7 @@
 layout: default
 title: Time-based One-Time Password
 parent: Configuration
-nav_order: 15
+nav_order: 16
 ---
 
 # Time-based One-Time Password
@@ -80,3 +80,13 @@ For example the default of 1 has a total of 3 keys valid. A value of 2 has 5 one
 valid.
 
 It is recommended to keep this value set to 0 or 1, the minimum is 0.
+
+## System time accuracy
+
+It's important to note that if the system time is not accurate enough then clients will seemingly not generate valid
+passwords for TOTP. Conversely this is the same when the client time is not accurate enough. This is due to the Time-based
+One Time Passwords being time-based.
+
+Authelia by default checks the system time against an [NTP server](./ntp.md#address) on startup. This helps to prevent
+a time synchronization issue on the server being an issue. There is however no effective and reliable way to check the
+clients.
\ No newline at end of file
diff --git a/docs/configuration/regulation.md b/docs/configuration/regulation.md
index 14abfa0b..5f3815fc 100644
--- a/docs/configuration/regulation.md
+++ b/docs/configuration/regulation.md
@@ -2,7 +2,7 @@
 layout: default
 title: Regulation
 parent: Configuration
-nav_order: 9
+nav_order: 10
 ---
 
 # Regulation
diff --git a/docs/configuration/secrets.md b/docs/configuration/secrets.md
index 5a2d2dce..08f75183 100644
--- a/docs/configuration/secrets.md
+++ b/docs/configuration/secrets.md
@@ -2,7 +2,7 @@
 layout: default
 title: Secrets
 parent: Configuration
-nav_order: 10
+nav_order: 11
 ---
 
 # Secrets
diff --git a/docs/configuration/server.md b/docs/configuration/server.md
index 3acbb509..1ccae5f6 100644
--- a/docs/configuration/server.md
+++ b/docs/configuration/server.md
@@ -2,7 +2,7 @@
 layout: default
 title: Server
 parent: Configuration
-nav_order: 11
+nav_order: 12
 ---
 
 # Server
diff --git a/docs/configuration/session/index.md b/docs/configuration/session/index.md
index 34bb6d0a..07aa23d9 100644
--- a/docs/configuration/session/index.md
+++ b/docs/configuration/session/index.md
@@ -2,7 +2,7 @@
 layout: default
 title: Session
 parent: Configuration
-nav_order: 12
+nav_order: 13
 has_children: true
 ---
 
diff --git a/docs/configuration/storage/index.md b/docs/configuration/storage/index.md
index 43a711d5..347fa26b 100644
--- a/docs/configuration/storage/index.md
+++ b/docs/configuration/storage/index.md
@@ -2,7 +2,7 @@
 layout: default
 title: Storage Backends
 parent: Configuration
-nav_order: 13
+nav_order: 14
 has_children: true
 ---
 
diff --git a/docs/configuration/theme.md b/docs/configuration/theme.md
index 3929ac8e..58ff93bc 100644
--- a/docs/configuration/theme.md
+++ b/docs/configuration/theme.md
@@ -2,7 +2,7 @@
 layout: default
 title: Theme
 parent: Configuration
-nav_order: 14
+nav_order: 15
 ---
 
 # Theme
diff --git a/internal/commands/root.go b/internal/commands/root.go
index c2fc0a8e..c36176d2 100644
--- a/internal/commands/root.go
+++ b/internal/commands/root.go
@@ -12,6 +12,7 @@ import (
 	"github.com/authelia/authelia/v4/internal/logging"
 	"github.com/authelia/authelia/v4/internal/middlewares"
 	"github.com/authelia/authelia/v4/internal/notification"
+	"github.com/authelia/authelia/v4/internal/ntp"
 	"github.com/authelia/authelia/v4/internal/oidc"
 	"github.com/authelia/authelia/v4/internal/regulation"
 	"github.com/authelia/authelia/v4/internal/server"
@@ -80,7 +81,10 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
 	server.Start(*config, providers)
 }
 
+//nolint:gocyclo // TODO: Consider refactoring time permitting.
 func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
+	logger := logging.Logger()
+
 	autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
 	if len(warnings) != 0 || len(errors) != 0 {
 		return providers, warnings, errors
@@ -133,6 +137,11 @@ func getProviders(config *schema.Configuration) (providers middlewares.Providers
 		}
 	}
 
+	var ntpProvider *ntp.Provider
+	if config.NTP != nil {
+		ntpProvider = ntp.NewProvider(config.NTP)
+	}
+
 	clock := utils.RealClock{}
 	authorizer := authorization.NewAuthorizer(config)
 	sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
@@ -143,12 +152,32 @@ func getProviders(config *schema.Configuration) (providers middlewares.Providers
 		errors = append(errors, err)
 	}
 
+	var failed bool
+	if !config.NTP.DisableStartupCheck && authorizer.IsSecondFactorEnabled() {
+		failed, err = ntpProvider.StartupCheck()
+
+		if err != nil {
+			logger.Errorf("Failed to check time against the NTP server: %+v", err)
+		}
+
+		if failed {
+			if config.NTP.DisableFailure {
+				logger.Error("The system time is outside the maximum desynchronization when compared to the time reported by the NTP server, this may cause issues in validating TOTP secrets")
+			} else {
+				logger.Fatal("The system time is outside the maximum desynchronization when compared to the time reported by the NTP server")
+			}
+		} else {
+			logger.Debug("The system time is within the maximum desynchronization when compared to the time reported by the NTP server")
+		}
+	}
+
 	return middlewares.Providers{
 		Authorizer:      authorizer,
 		UserProvider:    userProvider,
 		Regulator:       regulator,
 		OpenIDConnect:   oidcProvider,
 		StorageProvider: storageProvider,
+		NTP:             ntpProvider,
 		Notifier:        notifier,
 		SessionProvider: sessionProvider,
 	}, warnings, errors
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index b349511f..e21d4291 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -108,6 +108,29 @@ duo_api:
   ## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
   secret_key: 1234567890abcdefghifjkl
 
+##
+## NTP Configuration
+##
+## This is used to validate the servers time is accurate enough to validate TOTP.
+ntp:
+  ## NTP server address.
+  address: "time.cloudflare.com:123"
+
+  ## NTP version.
+  version: 4
+
+  ## Maximum allowed time offset between the host and the NTP server.
+  max_desync: 3s
+
+  ## Disables the NTP check on startup entirely. This means Authelia will not contact a remote service at all if you
+  ## set this to true, and can operate in a truly offline mode.
+  disable_startup_check: false
+
+  ## The default of false will prevent startup only if we can contact the NTP server and the time is out of sync with
+  ## the NTP server more than the configured max_desync. If you set this to true, an error will be logged but startup
+  ## will continue regardless of results.
+  disable_failure: false
+
 ##
 ## Authentication Backend Provider Configuration
 ##
diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go
index 47339ce7..60ed4f4b 100644
--- a/internal/configuration/schema/configuration.go
+++ b/internal/configuration/schema/configuration.go
@@ -22,6 +22,7 @@ type Configuration struct {
 	TOTP                  *TOTPConfiguration                 `koanf:"totp"`
 	DuoAPI                *DuoAPIConfiguration               `koanf:"duo_api"`
 	AccessControl         AccessControlConfiguration         `koanf:"access_control"`
+	NTP                   *NTPConfiguration                  `koanf:"ntp"`
 	Regulation            *RegulationConfiguration           `koanf:"regulation"`
 	Storage               StorageConfiguration               `koanf:"storage"`
 	Notifier              *NotifierConfiguration             `koanf:"notifier"`
diff --git a/internal/configuration/schema/ntp.go b/internal/configuration/schema/ntp.go
new file mode 100644
index 00000000..5ea9b67e
--- /dev/null
+++ b/internal/configuration/schema/ntp.go
@@ -0,0 +1,17 @@
+package schema
+
+// NTPConfiguration represents the configuration related to ntp server.
+type NTPConfiguration struct {
+	Address             string `koanf:"address"`
+	Version             int    `koanf:"version"`
+	MaximumDesync       string `koanf:"max_desync"`
+	DisableStartupCheck bool   `koanf:"disable_startup_check"`
+	DisableFailure      bool   `koanf:"disable_failure"`
+}
+
+// DefaultNTPConfiguration represents default configuration parameters for the NTP server.
+var DefaultNTPConfiguration = NTPConfiguration{
+	Address:       "time.cloudflare.com:123",
+	Version:       4,
+	MaximumDesync: "3s",
+}
diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go
index d23325db..0f5ec55b 100644
--- a/internal/configuration/validator/configuration.go
+++ b/internal/configuration/validator/configuration.go
@@ -65,4 +65,10 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
 	}
 
 	ValidateIdentityProviders(&configuration.IdentityProviders, validator)
+
+	if configuration.NTP == nil {
+		configuration.NTP = &schema.DefaultNTPConfiguration
+	}
+
+	ValidateNTP(configuration.NTP, validator)
 }
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index e238b7f5..b6339ca3 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -301,6 +301,13 @@ var ValidKeys = []string{
 	"identity_providers.oidc.clients[].scopes",
 	"identity_providers.oidc.clients[].grant_types",
 	"identity_providers.oidc.clients[].response_types",
+
+	// NTP keys.
+	"ntp.address",
+	"ntp.version",
+	"ntp.max_desync",
+	"ntp.disable_startup_check",
+	"ntp.disable_failure",
 }
 
 var replacedKeys = map[string]string{
diff --git a/internal/configuration/validator/ntp.go b/internal/configuration/validator/ntp.go
new file mode 100644
index 00000000..01cdd913
--- /dev/null
+++ b/internal/configuration/validator/ntp.go
@@ -0,0 +1,30 @@
+package validator
+
+import (
+	"fmt"
+
+	"github.com/authelia/authelia/v4/internal/configuration/schema"
+	"github.com/authelia/authelia/v4/internal/utils"
+)
+
+// ValidateNTP validates and update NTP configuration.
+func ValidateNTP(configuration *schema.NTPConfiguration, validator *schema.StructValidator) {
+	if configuration.Address == "" {
+		configuration.Address = schema.DefaultNTPConfiguration.Address
+	}
+
+	if configuration.Version == 0 {
+		configuration.Version = schema.DefaultNTPConfiguration.Version
+	} else if configuration.Version < 3 || configuration.Version > 4 {
+		validator.Push(fmt.Errorf("ntp: version must be either 3 or 4"))
+	}
+
+	if configuration.MaximumDesync == "" {
+		configuration.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync
+	}
+
+	_, err := utils.ParseDurationString(configuration.MaximumDesync)
+	if err != nil {
+		validator.Push(fmt.Errorf("ntp: error occurred parsing NTP max_desync string: %s", err))
+	}
+}
diff --git a/internal/configuration/validator/ntp_test.go b/internal/configuration/validator/ntp_test.go
new file mode 100644
index 00000000..f2bc29c9
--- /dev/null
+++ b/internal/configuration/validator/ntp_test.go
@@ -0,0 +1,65 @@
+package validator
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/authelia/authelia/v4/internal/configuration/schema"
+)
+
+func newDefaultNTPConfig() schema.NTPConfiguration {
+	config := schema.NTPConfiguration{}
+	return config
+}
+
+func TestShouldSetDefaultNtpAddress(t *testing.T) {
+	validator := schema.NewStructValidator()
+	config := newDefaultNTPConfig()
+
+	ValidateNTP(&config, validator)
+
+	assert.Len(t, validator.Errors(), 0)
+	assert.Equal(t, schema.DefaultNTPConfiguration.Address, config.Address)
+}
+
+func TestShouldSetDefaultNtpVersion(t *testing.T) {
+	validator := schema.NewStructValidator()
+	config := newDefaultNTPConfig()
+
+	ValidateNTP(&config, validator)
+
+	assert.Len(t, validator.Errors(), 0)
+	assert.Equal(t, schema.DefaultNTPConfiguration.Version, config.Version)
+}
+
+func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) {
+	validator := schema.NewStructValidator()
+	config := newDefaultNTPConfig()
+
+	ValidateNTP(&config, validator)
+
+	assert.Len(t, validator.Errors(), 0)
+	assert.Equal(t, schema.DefaultNTPConfiguration.MaximumDesync, config.MaximumDesync)
+}
+
+func TestShouldSetDefaultNtpDisableStartupCheck(t *testing.T) {
+	validator := schema.NewStructValidator()
+	config := newDefaultNTPConfig()
+
+	ValidateNTP(&config, validator)
+
+	assert.Len(t, validator.Errors(), 0)
+	assert.Equal(t, schema.DefaultNTPConfiguration.DisableStartupCheck, config.DisableStartupCheck)
+}
+
+func TestShouldRaiseErrorOnMaximumDesyncString(t *testing.T) {
+	validator := schema.NewStructValidator()
+	config := newDefaultNTPConfig()
+	config.MaximumDesync = "a second"
+
+	ValidateNTP(&config, validator)
+
+	assert.Len(t, validator.Errors(), 1)
+	assert.EqualError(t, validator.Errors()[0], "ntp: error occurred parsing NTP max_desync string: could not convert the input string of a second into a duration")
+}
diff --git a/internal/middlewares/types.go b/internal/middlewares/types.go
index f1e8467c..a0420919 100644
--- a/internal/middlewares/types.go
+++ b/internal/middlewares/types.go
@@ -9,6 +9,7 @@ import (
 	"github.com/authelia/authelia/v4/internal/authorization"
 	"github.com/authelia/authelia/v4/internal/configuration/schema"
 	"github.com/authelia/authelia/v4/internal/notification"
+	"github.com/authelia/authelia/v4/internal/ntp"
 	"github.com/authelia/authelia/v4/internal/oidc"
 	"github.com/authelia/authelia/v4/internal/regulation"
 	"github.com/authelia/authelia/v4/internal/session"
@@ -33,7 +34,7 @@ type Providers struct {
 	SessionProvider *session.Provider
 	Regulator       *regulation.Regulator
 	OpenIDConnect   oidc.OpenIDConnectProvider
-
+	NTP             *ntp.Provider
 	UserProvider    authentication.UserProvider
 	StorageProvider storage.Provider
 	Notifier        notification.Notifier
diff --git a/internal/ntp/const.go b/internal/ntp/const.go
new file mode 100644
index 00000000..11cb5c2c
--- /dev/null
+++ b/internal/ntp/const.go
@@ -0,0 +1,15 @@
+package ntp
+
+const (
+	ntpClientModeValue  uint8 = 3  // 00000011
+	ntpLeapEnabledValue uint8 = 64 // 01000000
+	ntpVersion3Value    uint8 = 24 // 00011000
+	ntpVersion4Value    uint8 = 40 // 00101000
+)
+
+const ntpEpochOffset = 2208988800
+
+const (
+	ntpV3 ntpVersion = iota
+	ntpV4
+)
diff --git a/internal/ntp/ntp.go b/internal/ntp/ntp.go
new file mode 100644
index 00000000..2ca914d6
--- /dev/null
+++ b/internal/ntp/ntp.go
@@ -0,0 +1,55 @@
+package ntp
+
+import (
+	"encoding/binary"
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/authelia/authelia/v4/internal/configuration/schema"
+	"github.com/authelia/authelia/v4/internal/utils"
+)
+
+// NewProvider instantiate a ntp provider given a configuration.
+func NewProvider(config *schema.NTPConfiguration) *Provider {
+	return &Provider{config}
+}
+
+// StartupCheck checks if the system clock is not out of sync.
+func (p *Provider) StartupCheck() (failed bool, err error) {
+	conn, err := net.Dial("udp", p.config.Address)
+	if err != nil {
+		return false, fmt.Errorf("could not connect to NTP server to validate the time desync: %w", err)
+	}
+
+	defer conn.Close()
+
+	if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
+		return false, fmt.Errorf("could not connect to NTP server to validate the time desync: %w", err)
+	}
+
+	version := ntpV4
+	if p.config.Version == 3 {
+		version = ntpV3
+	}
+
+	req := &ntpPacket{LeapVersionMode: ntpLeapVersionClientMode(false, version)}
+
+	if err := binary.Write(conn, binary.BigEndian, req); err != nil {
+		return false, fmt.Errorf("could not write to the NTP server socket to validate the time desync: %w", err)
+	}
+
+	now := time.Now()
+
+	resp := &ntpPacket{}
+
+	if err := binary.Read(conn, binary.BigEndian, resp); err != nil {
+		return false, fmt.Errorf("could not read from the NTP server socket to validate the time desync: %w", err)
+	}
+
+	maxOffset, _ := utils.ParseDurationString(p.config.MaximumDesync)
+
+	ntpTime := ntpPacketToTime(resp)
+
+	return ntpIsOffsetTooLarge(maxOffset, now, ntpTime), nil
+}
diff --git a/internal/ntp/ntp_test.go b/internal/ntp/ntp_test.go
new file mode 100644
index 00000000..3c578b96
--- /dev/null
+++ b/internal/ntp/ntp_test.go
@@ -0,0 +1,26 @@
+package ntp
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/authelia/authelia/v4/internal/configuration/schema"
+	"github.com/authelia/authelia/v4/internal/configuration/validator"
+)
+
+func TestShouldCheckNTP(t *testing.T) {
+	config := schema.NTPConfiguration{
+		Address:             "time.cloudflare.com:123",
+		Version:             4,
+		MaximumDesync:       "3s",
+		DisableStartupCheck: false,
+	}
+	sv := schema.NewStructValidator()
+	validator.ValidateNTP(&config, sv)
+
+	NTP := NewProvider(&config)
+
+	checkfailed, _ := NTP.StartupCheck()
+	assert.Equal(t, false, checkfailed)
+}
diff --git a/internal/ntp/types.go b/internal/ntp/types.go
new file mode 100644
index 00000000..7aa69dad
--- /dev/null
+++ b/internal/ntp/types.go
@@ -0,0 +1,30 @@
+package ntp
+
+import (
+	"github.com/authelia/authelia/v4/internal/configuration/schema"
+)
+
+// Provider type is the NTP provider.
+type Provider struct {
+	config *schema.NTPConfiguration
+}
+
+type ntpVersion int
+
+type ntpPacket struct {
+	LeapVersionMode       uint8
+	Stratum               uint8
+	Poll                  int8
+	Precision             int8
+	RootDelay             uint32
+	RootDispersion        uint32
+	ReferenceID           uint32
+	ReferenceTimeSeconds  uint32
+	ReferenceTimeFraction uint32
+	OriginTimeSeconds     uint32
+	OriginTimeFraction    uint32
+	RxTimeSeconds         uint32
+	RxTimeFraction        uint32
+	TxTimeSeconds         uint32
+	TxTimeFraction        uint32
+}
diff --git a/internal/ntp/util.go b/internal/ntp/util.go
new file mode 100644
index 00000000..fd6b6a83
--- /dev/null
+++ b/internal/ntp/util.go
@@ -0,0 +1,42 @@
+package ntp
+
+import "time"
+
+// ntpLeapVersionClientMode does the mathematics to configure the leap/version/mode value of an NTP client packet.
+func ntpLeapVersionClientMode(leap bool, version ntpVersion) (lvm uint8) {
+	lvm = ntpClientModeValue
+
+	if leap {
+		lvm += ntpLeapEnabledValue
+	}
+
+	switch version {
+	case ntpV3:
+		lvm += ntpVersion3Value
+	case ntpV4:
+		lvm += ntpVersion4Value
+	}
+
+	return lvm
+}
+
+// ntpPacketToTime converts a NTP server response into a time.Time.
+func ntpPacketToTime(packet *ntpPacket) time.Time {
+	seconds := float64(packet.TxTimeSeconds) - ntpEpochOffset
+	nanoseconds := (int64(packet.TxTimeFraction) * 1e9) >> 32
+
+	return time.Unix(int64(seconds), nanoseconds)
+}
+
+// ntpIsOffsetTooLarge return true if there is offset of "offset" between two times.
+func ntpIsOffsetTooLarge(maxOffset time.Duration, first, second time.Time) (tooLarge bool) {
+	var offset time.Duration
+
+	if first.After(second) {
+		offset = first.Sub(second)
+	} else {
+		offset = second.Sub(first)
+	}
+
+	return offset > maxOffset
+}
diff --git a/internal/ntp/util_test.go b/internal/ntp/util_test.go
new file mode 100644
index 00000000..53cb9b81
--- /dev/null
+++ b/internal/ntp/util_test.go
@@ -0,0 +1,16 @@
+package ntp
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/authelia/authelia/v4/internal/utils"
+)
+
+func TestShould(t *testing.T) {
+	maxOffset, _ := utils.ParseDurationString("1s")
+	assert.True(t, ntpIsOffsetTooLarge(maxOffset, time.Now(), time.Now().Add(time.Second*2)))
+	assert.False(t, ntpIsOffsetTooLarge(maxOffset, time.Now(), time.Now()))
+}
diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml
index 19fe89f0..5dec5e49 100644
--- a/internal/suites/Standalone/configuration.yml
+++ b/internal/suites/Standalone/configuration.yml
@@ -89,4 +89,13 @@ notifier:
     port: 1025
     sender: admin@example.com
     disable_require_tls: true
+ntp:
+  ## NTP server address
+  address: "time.cloudflare.com:123"
+  ## ntp version
+  version: 4
+  ## "maximum desynchronization" is the allowed offset time between the host and the ntp server
+  max_desync: 3s
+  ## You can enable or disable the NTP synchronization check on startup
+  disable_startup_check: false
 ...