mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
journal-remote: optionally record client certificate subjects
Add AddClientCertificateSubject= to record verified HTTPS client subjects in _REMOTE_CERTIFICATE_SUBJECT, allowing attribution to the immediate uploader even when multiple clients share an output journal. Default to disabled to preserve existing behavior. When enabled, replace incoming copies of the field and omit it for unauthenticated inputs. Signed-off-by: Everton de Matos <71781163+everton-dematos@users.noreply.github.com>
This commit is contained in:
1 parent
55326a3873
commit
f3f06acba7
9 files changed
+141
-1
No files matched your search
@@ -100,6 +100,18 @@
|
||||
<xi:include href="version-info.xml" xpointer="v220"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>AddClientCertificateSubject=</varname></term>
|
||||
|
||||
<listitem><para>Takes a boolean. If enabled, records the verified HTTPS client's full certificate
|
||||
subject in <varname>_REMOTE_CERTIFICATE_SUBJECT=</varname>. All incoming copies of this field are
|
||||
removed, including from non-HTTPS inputs and imported files. Without a verified client certificate,
|
||||
the field is omitted. Certificate verification and connection acceptance are unchanged.
|
||||
Defaults to no, leaving incoming fields unchanged.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>MaxUse=</varname></term>
|
||||
<term><varname>KeepFree=</varname></term>
|
||||
|
||||
@@ -456,6 +456,16 @@
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v252"/></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term><varname>_REMOTE_CERTIFICATE_SUBJECT=</varname></term>
|
||||
<listitem><para>The full distinguished name of the verified HTTPS uploader's certificate.
|
||||
Added by <command>systemd-journal-remote</command> with <varname>AddClientCertificateSubject=yes</varname>;
|
||||
see <citerefentry><refentrytitle>journal-remote.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
|
||||
Trust this field only from a receiver with this setting enabled. It identifies the immediate uploader,
|
||||
not the original producer of forwarded entries, and does not authenticate other entry fields.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "sd-journal.h"
|
||||
@@ -23,7 +24,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
_cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
|
||||
_cleanup_(unlink_and_freep) char *name = NULL;
|
||||
_cleanup_(sd_journal_closep) sd_journal *j = NULL;
|
||||
_cleanup_(journal_remote_server_destroy) RemoteServer s = {};
|
||||
_cleanup_(journal_remote_server_destroy) RemoteServer s = {
|
||||
.add_client_certificate_subject = size % 3 != 0,
|
||||
};
|
||||
int fdin, r;
|
||||
|
||||
if (outside_size_range(size, 3, 65536))
|
||||
@@ -53,6 +56,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
ASSERT_OK_POSITIVE(journal_remote_add_source(&s, fdin, (char*) "fuzz-data", false));
|
||||
TAKE_FD(fdin_close);
|
||||
|
||||
if (size % 3 == 2)
|
||||
assert_se(s.sources[fdin]->client_certificate_subject = strdup(REMOTE_CERTIFICATE_SUBJECT_FIELD "CN=fuzz"));
|
||||
|
||||
while (s.active)
|
||||
ASSERT_OK(journal_remote_handle_raw_source(NULL, fdin, 0, &s));
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ static char *arg_listen_https = NULL;
|
||||
static char **arg_files = NULL;
|
||||
static bool arg_compress = true;
|
||||
static bool arg_seal = false;
|
||||
static bool arg_add_client_certificate_subject = false;
|
||||
static int http_socket = -1, https_socket = -1;
|
||||
static char **arg_gnutls_log = NULL;
|
||||
|
||||
@@ -250,6 +251,12 @@ static int request_meta(void **connection_cls, int fd, char *_hostname) {
|
||||
if (!source)
|
||||
return log_oom();
|
||||
|
||||
if (journal_remote_server_global->add_client_certificate_subject && journal_remote_server_global->check_trust) {
|
||||
source->client_certificate_subject = strjoin(REMOTE_CERTIFICATE_SUBJECT_FIELD, source->importer.name);
|
||||
if (!source->client_certificate_subject)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
log_debug("Added RemoteSource as connection metadata %p", source);
|
||||
|
||||
r = build_accept_encoding(&source->encoding);
|
||||
@@ -849,6 +856,7 @@ static int parse_config(void) {
|
||||
{ "Remote", "MaxFiles", config_parse_uint64, 0, &arg_n_max_files },
|
||||
{ "Remote", "KeepFree", config_parse_iec_uint64, 0, &arg_keep_free },
|
||||
{ "Remote", "Compression", config_parse_compression, 0, &arg_compression },
|
||||
{ "Remote", "AddClientCertificateSubject", config_parse_bool, 0, &arg_add_client_certificate_subject },
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -1184,6 +1192,11 @@ static int run(int argc, char **argv) {
|
||||
s.check_trust = !arg_trust_all;
|
||||
}
|
||||
|
||||
s.add_client_certificate_subject = arg_add_client_certificate_subject;
|
||||
if (s.add_client_certificate_subject && !s.check_trust)
|
||||
log_notice("Client certificate verification is unavailable or disabled; "
|
||||
"removing incoming certificate subjects without adding a verified subject.");
|
||||
|
||||
journal_reset_metrics(&s.metrics);
|
||||
s.metrics.max_use = arg_max_use;
|
||||
s.metrics.max_size = arg_max_size;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "sd-event.h"
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "journal-remote-parse.h"
|
||||
#include "journal-remote.h"
|
||||
#include "log.h"
|
||||
#include "string-util.h"
|
||||
|
||||
RemoteSource* source_free(RemoteSource *source) {
|
||||
if (!source)
|
||||
@@ -19,6 +23,7 @@ RemoteSource* source_free(RemoteSource *source) {
|
||||
sd_event_source_unref(source->buffer_event);
|
||||
|
||||
free(source->encoding);
|
||||
free(source->client_certificate_subject);
|
||||
return mfree(source);
|
||||
}
|
||||
|
||||
@@ -71,6 +76,35 @@ int process_source(RemoteSource *source, JournalFileFlags file_flags) {
|
||||
|
||||
assert(source->importer.iovw.iovec);
|
||||
|
||||
if (source->writer->server->add_client_certificate_subject) {
|
||||
struct iovec_wrapper *iovw = &source->importer.iovw;
|
||||
size_t n = 0;
|
||||
|
||||
/* Drop all supplied copies, including binary fields and metadata from earlier hops. */
|
||||
FOREACH_ARRAY(i, iovw->iovec, iovw->count) {
|
||||
if (memory_startswith(i->iov_base, i->iov_len, REMOTE_CERTIFICATE_SUBJECT_FIELD))
|
||||
continue;
|
||||
iovw->iovec[n++] = *i;
|
||||
}
|
||||
iovw->count = n;
|
||||
|
||||
if (source->client_certificate_subject) {
|
||||
r = iovw_put(iovw, source->client_certificate_subject, strlen(source->client_certificate_subject));
|
||||
if (r == -E2BIG) {
|
||||
log_warning("Entry has no room for the client certificate subject, skipping.");
|
||||
r = 1;
|
||||
goto freeing;
|
||||
}
|
||||
if (r < 0)
|
||||
goto freeing;
|
||||
}
|
||||
|
||||
if (iovw_isempty(iovw)) {
|
||||
log_warning("Entry with no payload after removing certificate subjects, skipping.");
|
||||
goto freeing;
|
||||
}
|
||||
}
|
||||
|
||||
r = writer_write(source->writer,
|
||||
&source->importer.iovw,
|
||||
&source->importer.ts,
|
||||
|
||||
@@ -15,6 +15,7 @@ typedef struct RemoteSource {
|
||||
sd_event_source *buffer_event;
|
||||
Compression compression;
|
||||
char *encoding;
|
||||
char *client_certificate_subject;
|
||||
} RemoteSource;
|
||||
|
||||
RemoteSource* source_new(int fd, bool passive_fd, char *name, Writer *writer);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ServerKeyFile={{CERTIFICATE_ROOT}}/private/journal-remote.pem
|
||||
# ServerCertificateFile={{CERTIFICATE_ROOT}}/certs/journal-remote.pem
|
||||
# TrustedCertificateFile={{CERTIFICATE_ROOT}}/ca/trusted.pem
|
||||
# AddClientCertificateSubject=false
|
||||
# MaxUse=
|
||||
# KeepFree=
|
||||
# MaxFileSize=
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "journal-remote-parse.h"
|
||||
#include "journal-remote-write.h"
|
||||
|
||||
#define REMOTE_CERTIFICATE_SUBJECT_FIELD "_REMOTE_CERTIFICATE_SUBJECT="
|
||||
|
||||
typedef struct RemoteServer {
|
||||
RemoteSource **sources;
|
||||
size_t active;
|
||||
@@ -22,6 +24,7 @@ typedef struct RemoteServer {
|
||||
JournalWriteSplitMode split_mode;
|
||||
JournalFileFlags file_flags;
|
||||
bool check_trust;
|
||||
bool add_client_certificate_subject;
|
||||
JournalMetrics metrics;
|
||||
} RemoteServer;
|
||||
extern RemoteServer *journal_remote_server_global;
|
||||
|
||||
@@ -309,6 +309,66 @@ systemctl stop systemd-journal-upload
|
||||
systemctl stop systemd-journal-remote.{socket,service}
|
||||
rm -rf /var/log/journal/remote/*
|
||||
rm /run/systemd/journal-upload.conf.d/99-test.conf
|
||||
|
||||
# Reuse the trusted HTTPS receiver to check attribution and replacement of supplied fields.
|
||||
max_fields="$(getconf IOV_MAX)"
|
||||
for setting in '' yes; do
|
||||
if [[ -n "$setting" ]]; then
|
||||
echo "AddClientCertificateSubject=$setting" >>/run/systemd/journal-remote.conf.d/99-test.conf
|
||||
fi
|
||||
{
|
||||
printf '%s\n' '__REALTIME_TIMESTAMP=1700000000000000' '__MONOTONIC_TIMESTAMP=1000000' \
|
||||
'_BOOT_ID=0123456789abcdef0123456789abcdef' 'MESSAGE=field-limit-test'
|
||||
for ((i = 2; i < max_fields; i++)); do
|
||||
printf 'FIELD_%s=value\n' "$i"
|
||||
done
|
||||
printf '\n'
|
||||
printf '%s\n\n' '_REMOTE_CERTIFICATE_SUBJECT=CN=forged-only'
|
||||
printf '%s\n' '__REALTIME_TIMESTAMP=1700000000000000' '__MONOTONIC_TIMESTAMP=1000000' \
|
||||
'_BOOT_ID=0123456789abcdef0123456789abcdef' \
|
||||
'_REMOTE_CERTIFICATE_SUBJECT=CN=forged-one' 'MESSAGE=certificate-subject-test' \
|
||||
'_REMOTE_CERTIFICATE_SUBJECT=CN=forged-two' '_HOSTNAME=sender-claimed-host'
|
||||
printf '_REMOTE_CERTIFICATE_SUBJECT\n\x03\0\0\0\0\0\0\0a\0b\n\n'
|
||||
} >/run/systemd/remote-pki/subject.export
|
||||
systemctl restart systemd-journal-remote.socket
|
||||
for _ in 1 2; do
|
||||
curl --silent --show-error --fail --max-time 15 \
|
||||
--cacert /run/systemd/remote-pki/ca.crt \
|
||||
--cert /run/systemd/remote-pki/client.crt \
|
||||
--key /run/systemd/remote-pki/client.key \
|
||||
--header 'Content-Type: application/vnd.fdo.journal' \
|
||||
--data-binary @/run/systemd/remote-pki/subject.export https://localhost:19532/upload
|
||||
done
|
||||
expected_entries=6
|
||||
expected='["CN=forged-one","CN=forged-two",[97,0,98]]'
|
||||
if [[ "$setting" == yes ]]; then
|
||||
expected_entries=4
|
||||
expected=null
|
||||
if /usr/lib/systemd/systemd-journal-remote --version | grep -F +GNUTLS >/dev/null; then
|
||||
expected='"C=CZ,L=Brno,O=Foo,OU=Bar,CN=Test Client"'
|
||||
fi
|
||||
fi
|
||||
journalctl --directory=/var/log/journal/remote -o json --all |
|
||||
jq -se --argjson expected "$expected" --argjson count "$expected_entries" '
|
||||
length == $count and ([.[] | select(.MESSAGE == "certificate-subject-test")] |
|
||||
length == 2 and all(.[];
|
||||
._REMOTE_CERTIFICATE_SUBJECT == $expected and ._HOSTNAME == "sender-claimed-host"))'
|
||||
systemctl stop systemd-journal-remote.{socket,service}
|
||||
rm -rf /var/log/journal/remote/*
|
||||
|
||||
/usr/lib/systemd/systemd-journal-remote --split-mode=none \
|
||||
--output=/run/systemd/remote-pki/import.journal /run/systemd/remote-pki/subject.export
|
||||
expected_entries=3
|
||||
if [[ "$setting" == yes ]]; then
|
||||
expected_entries=2
|
||||
expected=null
|
||||
fi
|
||||
journalctl --file='/run/systemd/remote-pki/import*.journal' -o json --all |
|
||||
jq -se --argjson expected "$expected" --argjson count "$expected_entries" 'length == $count and
|
||||
([.[] | select(.MESSAGE == "certificate-subject-test")] | length == 1 and
|
||||
.[0]._REMOTE_CERTIFICATE_SUBJECT == $expected and .[0]._HOSTNAME == "sender-claimed-host")'
|
||||
rm /run/systemd/remote-pki/import*.journal
|
||||
done
|
||||
rm /run/systemd/journal-remote.conf.d/99-test.conf
|
||||
|
||||
DOS_DIR="$(mktemp -d)"
|
||||
|
||||
Reference in new issue
Block a user