feat(ntp): check clock sync on startup (#2251)
This adds method to validate the system clock is synchronized on startup. Configuration allows adjusting the server address, enabled state, desync limit, and if the error is fatal. Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>pull/2289/head^2
parent
fad6317bb5
commit
05406cfc7b
|
@ -108,6 +108,29 @@ duo_api:
|
||||||
## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||||
secret_key: 1234567890abcdefghifjkl
|
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
|
## Authentication Backend Provider Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -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.
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Time-based One-Time Password
|
title: Time-based One-Time Password
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 15
|
nav_order: 16
|
||||||
---
|
---
|
||||||
|
|
||||||
# Time-based One-Time Password
|
# 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.
|
valid.
|
||||||
|
|
||||||
It is recommended to keep this value set to 0 or 1, the minimum is 0.
|
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.
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Regulation
|
title: Regulation
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 9
|
nav_order: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
# Regulation
|
# Regulation
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Secrets
|
title: Secrets
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 10
|
nav_order: 11
|
||||||
---
|
---
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Server
|
title: Server
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 11
|
nav_order: 12
|
||||||
---
|
---
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Session
|
title: Session
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 12
|
nav_order: 13
|
||||||
has_children: true
|
has_children: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Storage Backends
|
title: Storage Backends
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 13
|
nav_order: 14
|
||||||
has_children: true
|
has_children: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Theme
|
title: Theme
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 14
|
nav_order: 15
|
||||||
---
|
---
|
||||||
|
|
||||||
# Theme
|
# Theme
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/logging"
|
"github.com/authelia/authelia/v4/internal/logging"
|
||||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||||
"github.com/authelia/authelia/v4/internal/notification"
|
"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/oidc"
|
||||||
"github.com/authelia/authelia/v4/internal/regulation"
|
"github.com/authelia/authelia/v4/internal/regulation"
|
||||||
"github.com/authelia/authelia/v4/internal/server"
|
"github.com/authelia/authelia/v4/internal/server"
|
||||||
|
@ -80,7 +81,10 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
|
||||||
server.Start(*config, providers)
|
server.Start(*config, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo // TODO: Consider refactoring time permitting.
|
||||||
func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
|
func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
|
||||||
|
logger := logging.Logger()
|
||||||
|
|
||||||
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
|
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
|
||||||
if len(warnings) != 0 || len(errors) != 0 {
|
if len(warnings) != 0 || len(errors) != 0 {
|
||||||
return providers, warnings, errors
|
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{}
|
clock := utils.RealClock{}
|
||||||
authorizer := authorization.NewAuthorizer(config)
|
authorizer := authorization.NewAuthorizer(config)
|
||||||
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
||||||
|
@ -143,12 +152,32 @@ func getProviders(config *schema.Configuration) (providers middlewares.Providers
|
||||||
errors = append(errors, err)
|
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{
|
return middlewares.Providers{
|
||||||
Authorizer: authorizer,
|
Authorizer: authorizer,
|
||||||
UserProvider: userProvider,
|
UserProvider: userProvider,
|
||||||
Regulator: regulator,
|
Regulator: regulator,
|
||||||
OpenIDConnect: oidcProvider,
|
OpenIDConnect: oidcProvider,
|
||||||
StorageProvider: storageProvider,
|
StorageProvider: storageProvider,
|
||||||
|
NTP: ntpProvider,
|
||||||
Notifier: notifier,
|
Notifier: notifier,
|
||||||
SessionProvider: sessionProvider,
|
SessionProvider: sessionProvider,
|
||||||
}, warnings, errors
|
}, warnings, errors
|
||||||
|
|
|
@ -108,6 +108,29 @@ duo_api:
|
||||||
## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||||
secret_key: 1234567890abcdefghifjkl
|
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
|
## Authentication Backend Provider Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -22,6 +22,7 @@ type Configuration struct {
|
||||||
TOTP *TOTPConfiguration `koanf:"totp"`
|
TOTP *TOTPConfiguration `koanf:"totp"`
|
||||||
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
|
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
|
||||||
AccessControl AccessControlConfiguration `koanf:"access_control"`
|
AccessControl AccessControlConfiguration `koanf:"access_control"`
|
||||||
|
NTP *NTPConfiguration `koanf:"ntp"`
|
||||||
Regulation *RegulationConfiguration `koanf:"regulation"`
|
Regulation *RegulationConfiguration `koanf:"regulation"`
|
||||||
Storage StorageConfiguration `koanf:"storage"`
|
Storage StorageConfiguration `koanf:"storage"`
|
||||||
Notifier *NotifierConfiguration `koanf:"notifier"`
|
Notifier *NotifierConfiguration `koanf:"notifier"`
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
|
@ -65,4 +65,10 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateIdentityProviders(&configuration.IdentityProviders, validator)
|
ValidateIdentityProviders(&configuration.IdentityProviders, validator)
|
||||||
|
|
||||||
|
if configuration.NTP == nil {
|
||||||
|
configuration.NTP = &schema.DefaultNTPConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateNTP(configuration.NTP, validator)
|
||||||
}
|
}
|
||||||
|
|
|
@ -301,6 +301,13 @@ var ValidKeys = []string{
|
||||||
"identity_providers.oidc.clients[].scopes",
|
"identity_providers.oidc.clients[].scopes",
|
||||||
"identity_providers.oidc.clients[].grant_types",
|
"identity_providers.oidc.clients[].grant_types",
|
||||||
"identity_providers.oidc.clients[].response_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{
|
var replacedKeys = map[string]string{
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/v4/internal/notification"
|
"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/oidc"
|
||||||
"github.com/authelia/authelia/v4/internal/regulation"
|
"github.com/authelia/authelia/v4/internal/regulation"
|
||||||
"github.com/authelia/authelia/v4/internal/session"
|
"github.com/authelia/authelia/v4/internal/session"
|
||||||
|
@ -33,7 +34,7 @@ type Providers struct {
|
||||||
SessionProvider *session.Provider
|
SessionProvider *session.Provider
|
||||||
Regulator *regulation.Regulator
|
Regulator *regulation.Regulator
|
||||||
OpenIDConnect oidc.OpenIDConnectProvider
|
OpenIDConnect oidc.OpenIDConnectProvider
|
||||||
|
NTP *ntp.Provider
|
||||||
UserProvider authentication.UserProvider
|
UserProvider authentication.UserProvider
|
||||||
StorageProvider storage.Provider
|
StorageProvider storage.Provider
|
||||||
Notifier notification.Notifier
|
Notifier notification.Notifier
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()))
|
||||||
|
}
|
|
@ -89,4 +89,13 @@ notifier:
|
||||||
port: 1025
|
port: 1025
|
||||||
sender: admin@example.com
|
sender: admin@example.com
|
||||||
disable_require_tls: true
|
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
|
||||||
...
|
...
|
||||||
|
|
Loading…
Reference in New Issue