fix(a7-3/s1): require TLS or local transport for daemon auth
Daemon modules that declare 'auth users' no longer accept credentials over a remote plaintext connection: server_module_gate refuses at the config gate, before any SCRAM challenge is sent, unless the connection is verified TLS with a client certificate matching --client-cn, or a local/SSH transport (loopback TCP peer or the --stdio pipe). --allow-unauthenticated does not relax this. The TLS client-CN comparison now uses credentials_secure_equal (S2). Clients sending --password-file to a non-loopback daemon must use --tls; validate_config rejects the plaintext case before any network I/O. Adds utils_sockaddr_is_loopback / utils_fd_peer_is_local / utils_host_is_loopback helpers with unit tests, a client validation unit test, and integration tests for the client-side plaintext rejection and the wrong-CN gate refusal.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include "delay_updates.h"
|
||||
#include "log.h"
|
||||
#include "usage.h"
|
||||
#include "utils.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -136,6 +137,15 @@ bool validate_config(const Config* config) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/* Daemon credentials (A7, protocol 2.19.0): a --password-file would send the
|
||||
username in the clear and derive a SCRAM proof a network sniffer could
|
||||
attack offline, so it is only allowed over TLS (which itself mandates a
|
||||
verified --cert/--key/--ca set above) or to a loopback destination. A
|
||||
remote plaintext daemon is refused here, before any network I/O. */
|
||||
if (config->password_file && !config->use_tls && !utils_host_is_loopback(config->server_host)) {
|
||||
log_message(LOG_LEVEL_ERROR, "sending daemon credentials to a non-local server requires --tls");
|
||||
return false;
|
||||
}
|
||||
if (config->delay_updates && config->inplace) {
|
||||
log_message(LOG_LEVEL_ERROR, "--delay-updates does not work with --inplace");
|
||||
return false;
|
||||
|
||||
+17
-5
@@ -173,7 +173,7 @@ static bool tls_client_identity_allowed(SSL* ssl) {
|
||||
size_t required_length = strlen(required_client_cn);
|
||||
bool allowed = length >= 0 && (size_t)length == required_length &&
|
||||
required_length < sizeof(common_name) &&
|
||||
memcmp(common_name, required_client_cn, required_length) == 0;
|
||||
credentials_secure_equal(common_name, required_client_cn, required_length);
|
||||
X509_free(certificate);
|
||||
return allowed;
|
||||
}
|
||||
@@ -382,11 +382,23 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
return "requested daemon module requires authentication and no credential "
|
||||
"store is configured";
|
||||
}
|
||||
if (gate_ctx && !gate_ctx->ssl) {
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"daemon module '%s' is authenticating over a plaintext connection (no --tls); "
|
||||
"the credential exchange is not encrypted",
|
||||
/* Transport policy (A7-3/S1): an auth-required module only accepts
|
||||
* credentials over an encrypted, verified TLS connection whose client
|
||||
* certificate matches --client-cn, or over a local/SSH transport (a
|
||||
* loopback TCP peer, or the --stdio pipe). A remote plaintext peer is
|
||||
* refused HERE, before the challenge is sent, so an unverified client never
|
||||
* receives a nonce. --allow-unauthenticated is intentionally NOT consulted:
|
||||
* that flag relaxes the standalone plaintext gate, never this one. */
|
||||
bool tls_ok = gate_ctx && gate_ctx->ssl && SSL_get_verify_result(gate_ctx->ssl) == X509_V_OK &&
|
||||
tls_client_identity_allowed(gate_ctx->ssl);
|
||||
bool local_ok = gate_ctx && gate_ctx->fd >= 0 && utils_fd_peer_is_local(gate_ctx->fd);
|
||||
if (!tls_ok && !local_ok) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"daemon module '%s' requires authentication over an encrypted, verified TLS "
|
||||
"connection (or a local/SSH transport); refusing",
|
||||
config->module);
|
||||
return "daemon module requires authentication over an encrypted, verified TLS "
|
||||
"connection";
|
||||
}
|
||||
if (!gate_ctx || gate_ctx->fd < 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#include "utils.h"
|
||||
#include "array_list.h"
|
||||
#include "log.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -543,3 +546,67 @@ bool append_tail_length(unsigned long long old_size, unsigned long long check_si
|
||||
*tail_out = check_size - old_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* True when a bound/peer socket address is on the loopback interface: any
|
||||
127.0.0.0/8 IPv4 address, IPv6 ::1, or an IPv4-mapped ::ffff:127.x.x.x. This
|
||||
is the transport-local test the daemon auth gate uses to decide whether a
|
||||
plaintext connection is a trustworthy local/SSH channel. */
|
||||
bool utils_sockaddr_is_loopback(const struct sockaddr* addr) {
|
||||
if (!addr)
|
||||
return false;
|
||||
if (addr->sa_family == AF_INET) {
|
||||
const struct sockaddr_in* v4 = (const struct sockaddr_in*)addr;
|
||||
uint32_t host = ntohl(v4->sin_addr.s_addr);
|
||||
return (host & 0xff000000u) == 0x7f000000u;
|
||||
}
|
||||
if (addr->sa_family == AF_INET6) {
|
||||
const struct sockaddr_in6* v6 = (const struct sockaddr_in6*)addr;
|
||||
if (IN6_IS_ADDR_LOOPBACK(&v6->sin6_addr))
|
||||
return true;
|
||||
/* An IPv4-mapped ::ffff:127.x.x.x is loopback too. */
|
||||
if (IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr) && v6->sin6_addr.s6_addr[12] == 127)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* True when the fd's peer is a local channel: a loopback TCP peer, or a
|
||||
non-socket descriptor (the --stdio SSH transport is a pipe, so a failed
|
||||
getpeername with ENOTSOCK counts as local). Any other socket peer is not
|
||||
local. */
|
||||
bool utils_fd_peer_is_local(int fd) {
|
||||
if (fd < 0)
|
||||
return false;
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t length = sizeof(peer);
|
||||
if (getpeername(fd, (struct sockaddr*)&peer, &length) != 0)
|
||||
return errno == ENOTSOCK;
|
||||
return utils_sockaddr_is_loopback((const struct sockaddr*)&peer);
|
||||
}
|
||||
|
||||
/* True when a client-supplied host string names a loopback destination:
|
||||
"localhost", any 127.0.0.0/8 literal, "::1", or "[::1]". */
|
||||
bool utils_host_is_loopback(const char* host) {
|
||||
if (!host || host[0] == '\0')
|
||||
return false;
|
||||
if (strcmp(host, "localhost") == 0)
|
||||
return true;
|
||||
struct in_addr v4;
|
||||
if (inet_pton(AF_INET, host, &v4) == 1)
|
||||
return (ntohl(v4.s_addr) & 0xff000000u) == 0x7f000000u;
|
||||
struct in6_addr addr6;
|
||||
if (host[0] == '[') {
|
||||
size_t len = strlen(host);
|
||||
if (len < 3 || host[len - 1] != ']')
|
||||
return false;
|
||||
/* inet_pton needs the bare address, not the bracketed form. */
|
||||
char bare[INET6_ADDRSTRLEN];
|
||||
if (len - 2 >= sizeof(bare))
|
||||
return false;
|
||||
memcpy(bare, host + 1, len - 2);
|
||||
bare[len - 2] = '\0';
|
||||
return inet_pton(AF_INET6, bare, &addr6) == 1 && IN6_IS_ADDR_LOOPBACK(&addr6);
|
||||
}
|
||||
return inet_pton(AF_INET6, host, &addr6) == 1 && IN6_IS_ADDR_LOOPBACK(&addr6);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "array_list.h"
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
char* str_dup(const char* string);
|
||||
char* output_escape(const char* string, bool eight_bit_output);
|
||||
@@ -66,5 +67,10 @@ bool format_human_bytes(unsigned long long bytes, char* buffer, size_t buffer_si
|
||||
bool append_resume_eligible(unsigned long long old_size, unsigned long long check_size);
|
||||
bool append_tail_length(unsigned long long old_size, unsigned long long check_size,
|
||||
unsigned long long* tail_out);
|
||||
/* Loopback / local-transport classification for the daemon auth gate and the
|
||||
client credential rule. See utils.c for the exact accepted forms. */
|
||||
bool utils_sockaddr_is_loopback(const struct sockaddr* addr);
|
||||
bool utils_fd_peer_is_local(int fd);
|
||||
bool utils_host_is_loopback(const char* host);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user