sources: enable selection options with authentication

When authentication is enabled for an NTP source, unauthenticated NTP
sources need to be disabled or limited in selection. That might be
difficult to do when the configuration comes from different sources
(e.g. networking scripts adding servers from DHCP).

Define four modes for the source selection to consider authentication:
require, prefer, mix, ignore. In different modes different selection
options (require, trust, noselect) are added to authenticated and
unauthenticated sources.

The mode can be selected by the authselectmode directive. The mix mode
is the default. The ignore mode enables the old behavior, where all
sources are used exactly as specified in the configuration.
This commit is contained in:
Miroslav Lichvar 2020-05-06 13:02:45 +02:00
parent dfe877144a
commit bddb3b3228
7 changed files with 278 additions and 9 deletions

29
conf.c
View file

@ -51,6 +51,7 @@ static int parse_double(char *line, double *result);
static int parse_null(char *line);
static void parse_allow_deny(char *line, ARR_Instance restrictions, int allow);
static void parse_authselectmode(char *);
static void parse_bindacqaddress(char *);
static void parse_bindaddress(char *);
static void parse_bindcmdaddress(char *);
@ -89,6 +90,7 @@ static double max_clock_error = 1.0; /* in ppm */
static double max_drift = 500000.0; /* in ppm */
static double max_slew_rate = 1e6 / 12.0; /* in ppm */
static SRC_AuthSelectMode authselect_mode = SRC_AUTHSELECT_MIX;
static double max_distance = 3.0;
static double max_jitter = 1.0;
static double reselect_distance = 1e-4;
@ -461,6 +463,8 @@ CNF_ParseLine(const char *filename, int number, char *line)
parse_int(p, &acquisition_port);
} else if (!strcasecmp(command, "allow")) {
parse_allow_deny(p, ntp_restrictions, 1);
} else if (!strcasecmp(command, "authselectmode")) {
parse_authselectmode(p);
} else if (!strcasecmp(command, "bindacqaddress")) {
parse_bindacqaddress(p);
} else if (!strcasecmp(command, "bindaddress")) {
@ -1141,6 +1145,23 @@ parse_allow_deny(char *line, ARR_Instance restrictions, int allow)
/* ================================================== */
static void
parse_authselectmode(char *line)
{
if (!strcasecmp(line, "require"))
authselect_mode = SRC_AUTHSELECT_REQUIRE;
else if (!strcasecmp(line, "prefer"))
authselect_mode = SRC_AUTHSELECT_PREFER;
else if (!strcasecmp(line, "mix"))
authselect_mode = SRC_AUTHSELECT_MIX;
else if (!strcasecmp(line, "ignore"))
authselect_mode = SRC_AUTHSELECT_IGNORE;
else
command_parse_error();
}
/* ================================================== */
static void
parse_bindacqaddress(char *line)
{
@ -1680,6 +1701,14 @@ CNF_GetCorrectionTimeRatio(void)
/* ================================================== */
SRC_AuthSelectMode
CNF_GetAuthSelectMode(void)
{
return authselect_mode;
}
/* ================================================== */
double
CNF_GetMaxSlewRate(void)
{

2
conf.h
View file

@ -30,6 +30,7 @@
#include "addressing.h"
#include "reference.h"
#include "sources.h"
extern void CNF_Initialise(int restarted, int client_only);
extern void CNF_Finalise(void);
@ -87,6 +88,7 @@ extern double CNF_GetMaxDrift(void);
extern double CNF_GetCorrectionTimeRatio(void);
extern double CNF_GetMaxSlewRate(void);
extern SRC_AuthSelectMode CNF_GetAuthSelectMode(void);
extern double CNF_GetMaxDistance(void);
extern double CNF_GetMaxJitter(void);
extern double CNF_GetReselectDistance(void);

View file

@ -739,6 +739,74 @@ with correct time.
=== Source selection
[[authselectmode]]*authselectmode* _mode_::
NTP sources can be specified with the *key* or *nts* option to enable
authentication to limit the impact of man-in-the-middle attacks. The
attackers can drop or delay NTP packets (up to the *maxdelay* and
<<maxdistance,*maxdistance*>> limits), but they cannot modify the timestamps
contained in the packets. The attack can cause only a limited slew or step, and
also cause the clock to run faster or slower than real time (up to double of
the <<maxdrift,*maxdrift*>> limit).
+
When authentication is enabled for an NTP source, it is important to disable
unauthenticated NTP sources which could be exploited in the attack, e.g. if
they are not reachable only over a trusted network. Alternatively, the source
selection can be configured with the *require* and *trust* options to
synchronise to the unauthenticated sources only if they agree with the
authenticated sources and might have a positive impact on the accuracy of the
clock. Note that in this case the impact of the attack is higher. The attackers
cannot cause an arbitrarily large step or slew, but they have more control over
the frequency of the clock and can cause *chronyd* to report false information,
e.g. a significantly smaller root delay and dispersion.
+
This directive determines the default selection options for authenticated and
unauthenticated sources in order to simplify the configuration with the
configuration file and *chronyc* commands. It sets a policy for authentication.
+
There are four modes:
+
*require*:::
Authentication is strictly required for NTP sources in this mode. If any
unauthenticated NTP sources are specified, they will automatically get the
*noselect* option to prevent them from being selected for synchronisation.
*prefer*:::
In this mode, authentication is optional and preferred. If it is enabled for at
least one NTP source, all unauthenticated NTP sources will get the *noselect*
option.
*mix*:::
In this mode, authentication is optional and synchronisation to a mix of
authenticated and unauthenticated NTP sources is allowed. If both authenticated
and unauthenticated NTP sources are specified, all authenticated NTP sources
and reference clocks will get the *require* and *trust* options to prevent
synchronisation to unauthenticated NTP sources if they do not agree with a
majority of the authenticated sources and reference clocks. This is the default
mode.
*ignore*:::
In this mode, authentication is ignored in the source selection. All sources
will have only the selection options that were specified in the configuration
file, or *chronyc* command. This was the behaviour of *chronyd* in versions
before 4.0.
::
+
As an example, the following configuration using the default *mix* mode:
+
----
server foo.example.net nts
server bar.example.net nts
server baz.example.net
refclock SHM 0
----
+
is equivalent to the following configuration using the *ignore* mode:
+
----
authselectmode ignore
server foo.example.net nts require trust
server bar.example.net nts require trust
server baz.example.net
refclock SHM 0 require trust
----
[[combinelimit]]*combinelimit* _limit_::
When *chronyd* has multiple sources available for synchronisation, it has to
select one source as the synchronisation source. The measured offsets and

View file

@ -495,10 +495,62 @@ SRC_ResetReachability(SRC_Instance inst)
static void
update_sel_options(void)
{
int i;
int options, auth_ntp_options, unauth_ntp_options, refclk_options;
int i, auth_ntp_sources, unauth_ntp_sources;
auth_ntp_sources = unauth_ntp_sources = 0;
for (i = 0; i < n_sources; i++) {
sources[i]->sel_options = sources[i]->conf_sel_options;
if (sources[i]->type != SRC_NTP)
continue;
if (sources[i]->authenticated)
auth_ntp_sources++;
else
unauth_ntp_sources++;
}
auth_ntp_options = unauth_ntp_options = refclk_options = 0;
/* Determine which selection options need to be added to authenticated NTP
sources, unauthenticated NTP sources, and refclocks, to follow the
configured selection mode */
switch (CNF_GetAuthSelectMode()) {
case SRC_AUTHSELECT_IGNORE:
break;
case SRC_AUTHSELECT_MIX:
if (auth_ntp_sources > 0 && unauth_ntp_sources > 0)
auth_ntp_options = refclk_options = SRC_SELECT_REQUIRE | SRC_SELECT_TRUST;
break;
case SRC_AUTHSELECT_PREFER:
if (auth_ntp_sources > 0)
unauth_ntp_options = SRC_SELECT_NOSELECT;
break;
case SRC_AUTHSELECT_REQUIRE:
unauth_ntp_options = SRC_SELECT_NOSELECT;
break;
default:
assert(0);
}
for (i = 0; i < n_sources; i++) {
options = sources[i]->conf_sel_options;
switch (sources[i]->type) {
case SRC_NTP:
options |= sources[i]->authenticated ? auth_ntp_options : unauth_ntp_options;
break;
case SRC_REFCLOCK:
options |= refclk_options;
break;
default:
assert(0);
}
if (sources[i]->sel_options != options) {
DEBUG_LOG("changing %s from %x to %x", source_to_string(sources[i]),
(unsigned int)sources[i]->sel_options, (unsigned int)options);
sources[i]->sel_options = options;
}
}
}

View file

@ -51,6 +51,14 @@ extern void SRC_Initialise(void);
/* Finalisation function */
extern void SRC_Finalise(void);
/* Modes for selecting NTP sources based on their authentication status */
typedef enum {
SRC_AUTHSELECT_IGNORE,
SRC_AUTHSELECT_MIX,
SRC_AUTHSELECT_PREFER,
SRC_AUTHSELECT_REQUIRE,
} SRC_AuthSelectMode;
typedef enum {
SRC_NTP, /* NTP client/peer */
SRC_REFCLOCK /* Rerefence clock */

View file

@ -65,4 +65,28 @@ check_packet_interval || test_fail
check_source_selection && test_fail
check_sync && test_fail
cat > tmp/keys <<-EOF
1 MD5 HEX:1B81CBF88D4A73F2E8CE59647F6E5C1719B6CAF5
EOF
server_conf="keyfile tmp/keys"
client_server_conf="
server 192.168.123.1 key 1
server 192.168.123.2
server 192.168.123.3"
for authselectmode in require prefer mix ignore; do
client_conf="keyfile tmp/keys
authselectmode $authselectmode"
run_test || test_fail
check_chronyd_exit || test_fail
check_source_selection || test_fail
check_packet_interval || test_fail
if [ $authselectmode = ignore ]; then
check_sync || test_fail
else
check_sync && test_fail
fi
done
test_pass

View file

@ -21,14 +21,27 @@
#include <sources.c>
#include "test.h"
static SRC_Instance
create_source(SRC_Type type, int authenticated, int sel_options)
{
static IPAddr addr;
TST_GetRandomAddress(&addr, IPADDR_UNSPEC, -1);
return SRC_CreateNewInstance(UTI_IPToRefid(&addr), type, authenticated, sel_options,
type == SRC_NTP ? &addr : NULL,
SRC_DEFAULT_MINSAMPLES, SRC_DEFAULT_MAXSAMPLES, 0.0, 1.0);
}
void
test_unit(void)
{
SRC_AuthSelectMode sel_mode;
SRC_Instance srcs[16];
RPT_SourceReport report;
NTP_Sample sample;
IPAddr addr;
int i, j, k, l, samples, sel_options;
int i, j, k, l, n1, n2, n3, samples, sel_options;
char conf[128];
CNF_Initialise(0, 0);
LCL_Initialise();
@ -45,15 +58,11 @@ test_unit(void)
for (j = 0; j < sizeof (srcs) / sizeof (srcs[0]); j++) {
TEST_CHECK(n_sources == j);
TST_GetRandomAddress(&addr, IPADDR_UNSPEC, -1);
sel_options = i & random() & (SRC_SELECT_NOSELECT | SRC_SELECT_PREFER |
SRC_SELECT_TRUST | SRC_SELECT_REQUIRE);
DEBUG_LOG("added source %d options %d", j, sel_options);
srcs[j] = SRC_CreateNewInstance(UTI_IPToRefid(&addr), SRC_NTP, 0, sel_options, &addr,
SRC_DEFAULT_MINSAMPLES, SRC_DEFAULT_MAXSAMPLES,
0.0, 1.0);
srcs[j] = create_source(SRC_NTP, 0, sel_options);
SRC_UpdateReachability(srcs[j], 1);
samples = (i + j) % 5 + 3;
@ -132,6 +141,83 @@ test_unit(void)
}
}
TEST_CHECK(CNF_GetAuthSelectMode() == SRC_AUTHSELECT_MIX);
for (i = 0; i < 1000; i++) {
DEBUG_LOG("iteration %d", i);
switch (i % 4) {
case 0:
snprintf(conf, sizeof (conf), "authselectmode require");
sel_mode = SRC_AUTHSELECT_REQUIRE;
break;
case 1:
snprintf(conf, sizeof (conf), "authselectmode prefer");
sel_mode = SRC_AUTHSELECT_PREFER;
break;
case 2:
snprintf(conf, sizeof (conf), "authselectmode mix");
sel_mode = SRC_AUTHSELECT_MIX;
break;
case 3:
snprintf(conf, sizeof (conf), "authselectmode ignore");
sel_mode = SRC_AUTHSELECT_IGNORE;
break;
}
CNF_ParseLine(NULL, 0, conf);
TEST_CHECK(CNF_GetAuthSelectMode() == sel_mode);
sel_options = random() & (SRC_SELECT_NOSELECT | SRC_SELECT_PREFER |
SRC_SELECT_TRUST | SRC_SELECT_REQUIRE);
n1 = random() % 3;
n2 = random() % 3;
n3 = random() % 3;
assert(n1 + n2 + n3 < sizeof (srcs) / sizeof (srcs[0]));
for (j = 0; j < n1; j++)
srcs[j] = create_source(SRC_REFCLOCK, random() % 2, sel_options);
for (; j < n1 + n2; j++)
srcs[j] = create_source(SRC_NTP, 1, sel_options);
for (; j < n1 + n2 + n3; j++)
srcs[j] = create_source(SRC_NTP, 0, sel_options);
switch (sel_mode) {
case SRC_AUTHSELECT_IGNORE:
for (j = 0; j < n1 + n2 + n3; j++)
TEST_CHECK(srcs[j]->sel_options == sel_options);
break;
case SRC_AUTHSELECT_MIX:
for (j = 0; j < n1 + n2; j++)
TEST_CHECK(srcs[j]->sel_options ==
(sel_options | (n2 > 0 && n3 > 0 ? SRC_SELECT_REQUIRE | SRC_SELECT_TRUST : 0)));
for (; j < n1 + n2 + n3; j++)
TEST_CHECK(srcs[j]->sel_options == sel_options);
break;
case SRC_AUTHSELECT_PREFER:
for (j = 0; j < n1 + n2; j++)
TEST_CHECK(srcs[j]->sel_options == sel_options);
for (; j < n1 + n2 + n3; j++)
TEST_CHECK(srcs[j]->sel_options == (sel_options | (n2 > 0 ? SRC_SELECT_NOSELECT : 0)));
break;
case SRC_AUTHSELECT_REQUIRE:
for (j = 0; j < n1 + n2; j++)
TEST_CHECK(srcs[j]->sel_options == sel_options);
for (; j < n1 + n2 + n3; j++)
TEST_CHECK(srcs[j]->sel_options == (sel_options | SRC_SELECT_NOSELECT));
break;
default:
assert(0);
}
for (j = n1 + n2 + n3 - 1; j >= 0; j--) {
if (j < n1 + n2)
TEST_CHECK(srcs[j]->sel_options == sel_options);
SRC_DestroyInstance(srcs[j]);
}
}
REF_Finalise();
SRC_Finalise();
SCH_Finalise();