tls: Limit concurrent connections

Resolves: COCKPIT-1905, RHEL-263070, RHEL-263066
This commit is contained in:
Marius Vollmer 2026-09-15 13:36:04 +03:00
parent 709aac3638
commit e12c37897d
5 changed files with 79 additions and 4 deletions

View file

@ -92,6 +92,12 @@ ForwardedForHeader = X-Forwarded-For
unauthenticated connections. The probability increases linearly and unauthenticated connections. The probability increases linearly and
all connection attempts are refused if the number of unauthenticated all connection attempts are refused if the number of unauthenticated
connections reaches *full* (60). connections reaches *full* (60).
*MaxConcurrentConnections*::
Limits the number of concurrent HTTP connections on the Cockpit port.
Connection attempts that exceed this limit are rejected early on
without reading a single byte. This can be used during brute-force
attacks. It differs from *MaxStartups*, which protects against too
many concurrent authentication attempts. Defaults to 500.
*AllowUnencrypted*:: *AllowUnencrypted*::
If true, cockpit will accept unencrypted HTTP connections. Otherwise, If true, cockpit will accept unencrypted HTTP connections. Otherwise,
it redirects all HTTP connections to HTTPS. Exceptions are connections it redirects all HTTP connections to HTTPS. Exceptions are connections

View file

@ -94,7 +94,9 @@ main (int argc, char **argv)
if (!runtimedir) if (!runtimedir)
errx (EXIT_FAILURE, "$RUNTIME_DIRECTORY environment variable must be set to a private directory"); errx (EXIT_FAILURE, "$RUNTIME_DIRECTORY environment variable must be set to a private directory");
server_init ("/run/cockpit/wsinstance", runtimedir, arguments.idle_timeout, arguments.port); unsigned int max_connections = cockpit_conf_uint ("WebService", "MaxConcurrentConnections", 500, 10, UINT_MAX);
server_init ("/run/cockpit/wsinstance", runtimedir, arguments.idle_timeout, arguments.port, max_connections);
if (!arguments.no_tls) if (!arguments.no_tls)
{ {

View file

@ -37,6 +37,8 @@ static struct {
/* rw, protected by mutex */ /* rw, protected by mutex */
pthread_mutex_t connection_mutex; pthread_mutex_t connection_mutex;
unsigned int connection_count; unsigned int connection_count;
unsigned int max_connection_count;
bool max_connection_warning_issued;
int idle_timerfd; int idle_timerfd;
struct itimerspec idle_timeout; struct itimerspec idle_timeout;
} server; } server;
@ -134,6 +136,26 @@ handle_accept (int listen_fd)
timerfd_settime (server.idle_timerfd, 0, &zero, NULL); timerfd_settime (server.idle_timerfd, 0, &zero, NULL);
} }
if (server.connection_count >= server.max_connection_count)
{
if (!server.max_connection_warning_issued)
{
warnx ("too many connections");
server.max_connection_warning_issued = true;
}
pthread_mutex_unlock (&server.connection_mutex);
// Send a immediate RST to the client to immediately free up
// our resources associated with the socket. This results in
// a "Connection reset" error on the other end, which is
// useful as well.
struct linger opt = { .l_onoff = 1, .l_linger = 0 };
setsockopt (fd, SOL_SOCKET, SO_LINGER, &opt, sizeof(opt));
close (fd);
return;
}
server.connection_count++; server.connection_count++;
debug (CONNECTION, " -> server.connection_count is now %i", server.connection_count); debug (CONNECTION, " -> server.connection_count is now %i", server.connection_count);
@ -186,13 +208,15 @@ void
server_init (const char *wsinstance_sockdir, server_init (const char *wsinstance_sockdir,
const char *cert_session_dir, const char *cert_session_dir,
int idle_timeout, int idle_timeout,
uint16_t port) uint16_t port,
unsigned int max_connection_count)
{ {
const char *env_listen_fds; const char *env_listen_fds;
struct epoll_event ev = { .events = EPOLLIN }; struct epoll_event ev = { .events = EPOLLIN };
assert (!server.initialized); assert (!server.initialized);
server.initialized = true; server.initialized = true;
server.max_connection_count = max_connection_count;
server.idle_timerfd = -1; server.idle_timerfd = -1;
connection_set_directories (wsinstance_sockdir, cert_session_dir); connection_set_directories (wsinstance_sockdir, cert_session_dir);

View file

@ -14,7 +14,8 @@ void
server_init (const char *wsinstance_sockdir, server_init (const char *wsinstance_sockdir,
const char *cert_session_dir, const char *cert_session_dir,
int idle_timeout, int idle_timeout,
uint16_t port); uint16_t port,
unsigned int max_connections);
void void
server_run (void); server_run (void);

View file

@ -61,8 +61,13 @@ typedef struct {
const char *client_fingerprint; const char *client_fingerprint;
const char *priority; const char *priority;
int expected_pk_algo; int expected_pk_algo;
unsigned int max_connection_count;
} TestFixture; } TestFixture;
static const TestFixture fixture_max_connection_count_10 = {
.max_connection_count = 10,
};
static const TestFixture fixture_separate_crt_key = { static const TestFixture fixture_separate_crt_key = {
.certfile = CERTFILE, .certfile = CERTFILE,
.keyfile = KEYFILE, .keyfile = KEYFILE,
@ -468,8 +473,10 @@ setup (TestCase *tc, gconstpointer data)
} }
close (socket_dir_fd); close (socket_dir_fd);
unsigned max_connection_count = (fixture && fixture->max_connection_count != 0) ? fixture->max_connection_count : 500;
/* Let the kernel assign a port */ /* Let the kernel assign a port */
server_init (tc->ws_socket_dir, tc->runtime_dir, fixture ? fixture->idle_timeout : 0, 0); server_init (tc->ws_socket_dir, tc->runtime_dir, fixture ? fixture->idle_timeout : 0, 0, max_connection_count);
if (fixture && fixture->certfile) if (fixture && fixture->certfile)
{ {
@ -671,6 +678,39 @@ test_no_tls_many_parallel (TestCase *tc, gconstpointer data)
} }
} }
static void
test_no_tls_too_many_parallel (TestCase *tc, gconstpointer data)
{
int i;
int fds[20];
const char *request = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n";
for (i = 0; i < 20; ++i)
{
fds[i] = do_connect (tc);
g_assert_cmpint (fds[i], >, 0);
server_poll_event (50);
}
for (i = 0; i < 20; ++i)
{
if (i < 10)
{
/* the first 10 are allowed to connect and will receive a reply */
do_request_fd (fds[i], request);
}
else
{
/* the rest gets disconnected immediately */
int r = write (fds[i], request, strlen (request));
g_assert (r == -1 && errno == ECONNRESET);
}
close(fds[i]);
}
}
static void static void
test_no_tls_redirect (TestCase *tc, gconstpointer data) test_no_tls_redirect (TestCase *tc, gconstpointer data)
{ {
@ -970,6 +1010,8 @@ main (int argc, char *argv[])
setup, test_no_tls_many_serial, teardown); setup, test_no_tls_many_serial, teardown);
g_test_add ("/server/no-tls/many-parallel", TestCase, NULL, g_test_add ("/server/no-tls/many-parallel", TestCase, NULL,
setup, test_no_tls_many_parallel, teardown); setup, test_no_tls_many_parallel, teardown);
g_test_add ("/server/no-tls/too-many-parallel", TestCase, &fixture_max_connection_count_10,
setup, test_no_tls_too_many_parallel, teardown);
g_test_add ("/server/no-tls/redirect", TestCase, NULL, g_test_add ("/server/no-tls/redirect", TestCase, NULL,
setup, test_no_tls_redirect, teardown); setup, test_no_tls_redirect, teardown);
g_test_add ("/server/tls/no-client-cert", TestCase, &fixture_separate_crt_key, g_test_add ("/server/tls/no-client-cert", TestCase, &fixture_separate_crt_key,