Conversation
Add an infix-syslog:log RPC that hands a message to the local syslog daemon. The full RFC 5424 header is exposed: severity, facility, app-name, msgid, and structured data. Defaults follow logger(1), user.notice, with app-name defaulting to the name of the calling user. The message is time stamped on arrival and goes through the regular /syslog filtering and forwarding rules, so where it ends up is decided by the device configuration, not the caller. The handler uses syslogp_r() from the sysklogd libsyslog, which passes msgid and structured data through and sets the tag per call without touching confd's own openlog() identity. The callback's event session runs as confd, so the calling user is taken from the event originator: netopeer2 pushes it as originator data, the CLI sets it as originator name. RESTCONF sets neither, there the originator name is used as-is. Fixes #1639 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Front-end to the infix-syslog:log RPC. Optional severity, facility, and msgid keywords precede the message, which is the rest of the line: admin@example:/> log severity warning msgid test-start Test 42 starting Two fixes to the rpc tool the command is built on. It now sets the session originator name to the calling user, like klish-plugin-sysrepo, so the RPC can default app-name to the user. And it only splits comma-separated values for leaf-lists, previously any leaf value with a comma became two instances and the RPC failed: $ rpc /infix-syslog:log message "Kilroy was here, again" Issue #1639 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Wrap the infix-syslog:log RPC so tests can leave markers in a DUT's system log over the management API instead of SSH and logger(1). Facility names are module-qualified in the helper, and structured data is given as a dict of dicts to keep call sites short. The RESTCONF backend's call_dict() only tracked coverage and returned without sending anything, so RPCs with input silently did nothing over RESTCONF. POST to /restconf/operations/MODULE:NAME with the input wrapped per RFC 8040, like call_action() does. Convert the tests that logged via SSH. The remote and hostname_filter tests keep using logger(1), they deliberately exercise the remote client path. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Log to an RFC 5424 formatted file and check app-name, msgid, and structured data land in the header, and that a bare message gets the default app-name with empty msgid and structured data. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
infix-syslog:log RPC for injecting messages in the system log
infix-syslog:log RPC for injecting messages in the system log
mattiaswal
left a comment
There was a problem hiding this comment.
Security review of the new log RPC. Verified against libyang (yanglint) with the exact type definitions from this PR, and against the sysklogd 2.7.2 sources Infix ships. Details inline; summary ranked by severity:
- High: format string injection via
msgid. libsyslog'svsyslogp_r()concatenatesmsgidinto the printf format it hands tovsnprintf(), and the pattern[!-~]+allows%. Runs inside confd as root. - Medium: log line forgery via
structured-data/param/value. Control characters are not escaped, syslogd does not sanitize SD (only MSG), andfmt5424writes it verbatim to RFC 5424 files and remote forwarding. - Medium: unbounded SD is truncated in libsyslog's 2048-byte buffer, after which syslogd's RFC 5424 parser drops the whole message silently.
- Medium (design): any user with
execrights (factory NACM:exec-default permit) can forge entries tagged as any daemon, inauth/authpriv, or atemergencyseverity (walled to all ttys and/dev/console). Onlykernis neutralized by syslogd. - Low: no rate limiting; libsyslog's
sendto()is blocking and confd's RPC callback isSR_SUBSCR_NO_THREAD. - Low: default app-name from
sr_session_get_orig_name()is client-controlled.
What I checked and found fine: the sd-name typedef matches RFC 5424 exactly (rejects =, ], ", SP); sd_build() length arithmetic is correct and sysrepo validates mandatory/key leaves before the callback; ASCII control characters in message are rejected by libyang (C0 except TAB/LF/CR) or neutralized by syslogd (LF to space, others to ^X); the klish script quoting and the rpc/copy changes are sound.
Note that -8 on syslogd disables C1 masking, so UTF-8 encoded C1 controls (U+0085, U+009B), U+2028 and bidi overrides in message reach log files and show log untouched. Low severity, mentioned for completeness.
| leaf msgid { | ||
| type string { | ||
| length "1..32"; | ||
| pattern '[!-~]+'; |
There was a problem hiding this comment.
High: format string injection. This pattern permits %, and sysklogd's vsyslogp_r() builds its printf format by concatenating msgid into it (sysklogd 2.7.2 src/syslog.c L451-452, then vsnprintf(p, tbuf_left, fmt_cpy, ap) at L507). A caller can therefore pass msgid = %s%s%s%s%s%s to crash confd, or %n... for a write primitive, and this runs as root. yanglint confirms %n%n%n%n is accepted by this type.
Suggest excluding % here, e.g. pattern '[!-$&-~]+';, and also escaping % as %% in rpc_log() so the C side does not depend on the model. app-name is not affected (libsyslog uses snprintf("%s ", tag)), and sd/message are passed as %s arguments.
There was a problem hiding this comment.
Awesome feedback, will look into this asap!
| log.log_tag = tag; | ||
| sd = sd_build(in); | ||
| if (sd) | ||
| syslogp_r(pri, &log, msgid, "%s", "%s", sd, msg); |
There was a problem hiding this comment.
See the comment on the msgid leaf: msgid is used as part of the format string inside syslogp_r(), not as a plain argument. Even with the YANG pattern tightened, I would escape % to %% here (or reject the value) before passing it on. The "%s" handling of sd and msg is correct.
There was a problem hiding this comment.
Will fix, thanks!
| } | ||
|
|
||
| leaf value { | ||
| type string; |
There was a problem hiding this comment.
Medium: log line forgery and silent message loss.
-
valueis an unrestricted string, andsd_escape()only escapes",\and]. libyang accepts LF, CR, DEL and C1 controls here. syslogd'sparsemsg_rfc5424()accepts any bytes inside PARAM-VALUE except an unescaped", keeps the SD block raw (buffer.sd = msg), andfmt5424()writes it verbatim. Only MSG goes throughparsemsg_remove_unsafe_characters(). So a value containing\nproduces a fabricated extra log line in everylog-format rfc5424file and in RFC 5424 remote forwarding. -
There is no bound on value length or list size, but libsyslog formats the whole packet into
tbuf[2048]. A long SD is truncated mid-element, syslogd'sFAIL_IF("STRUCTURED-NAME", ...)then drops the entire message, and the caller getsSR_ERR_OK.
Suggest something like length "1..255" plus a pattern excluding control characters, and escaping or stripping controls in sd_escape() as a belt-and-braces measure.
There was a problem hiding this comment.
I'll take a deeper look at this too, the formatting rules are strict but allows quite a lot still, so this needs some careful thinking. Might even spill over as fixes in the sysklogd project, so thank you! 🙏
| } | ||
|
|
||
| /* RFC 5424 PARAM-VALUE: escape '"', '\\', and ']' */ | ||
| static char *sd_escape(char *ptr, const char *value) |
There was a problem hiding this comment.
Consider also handling control characters here (e.g. replace anything < 0x20 and 0x7f with a space), since syslogd sanitizes MSG but not SD. See the comment on the value leaf for the forgery scenario.
| reference "RFC 5424: The Syslog Protocol"; | ||
|
|
||
| input { | ||
| leaf message { |
There was a problem hiding this comment.
FYI on control characters in message, since it came up: libyang enforces RFC 7950 string rules, so C0 controls other than TAB/LF/CR, surrogates and noncharacters are rejected at parse time (ESC cannot get in). LF/CR/DEL do get through, but syslogd's parsemsg_remove_unsafe_characters() turns LF into a space and the rest into ^X, so this leaf cannot forge a second log line. Because Infix runs syslogd -8, C1 masking is off, so UTF-8 encoded U+0085/U+009B, U+2028 and bidi overrides such as U+202E are logged as-is. Low severity; a pattern like [^\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+ would close it if wanted.
One more thing: length "1..2048" counts characters, while libsyslog's buffer is 2048 bytes for the entire packet including header and SD, so long messages are silently truncated. Worth mentioning in the description or lowering the limit.
| * RPCs | ||
| */ | ||
|
|
||
| rpc log { |
There was a problem hiding this comment.
Medium (design): forgery and authorization. Callers pick app-name, facility and severity freely. That allows entries in /var/log/auth.log tagged sshd with facility auth, or emergency messages that syslogd walls to every logged-in tty and to /dev/console (factory *.=emerg * and console.conf). Only kern is neutralized, since syslogd remaps LOG_KERN from /dev/log unless started with -k.
The factory NACM has exec-default permit, so operators and any non-guest user can call this. Options: a factory NACM rule limiting infix-syslog:log exec to admin (or an explicit operator permit), refusing auth/authpriv, and/or always adding a fixed marker (e.g. an SD element or msgid prefix) so injected messages stay distinguishable from genuine ones in forwarded logs. At minimum the docs should say that the header fields are caller-supplied and not authenticated.
There was a problem hiding this comment.
I was considering marking the RPC with nacm:default-deny-all;, so this is great feedback, thanks! It's the age-old question of flexibility vs security.
There was a problem hiding this comment.
Guess it can be good, we should really be more strict about access in general and use nacm:default-deny-all; more.
| return sd; | ||
| } | ||
|
|
||
| static int rpc_log(sr_session_ctx_t *session, uint32_t sub_id, const char *op_path, |
There was a problem hiding this comment.
Low: no rate limiting. Each call can write up to 2048 bytes to a typically RAM-backed /var/log, rotating away real evidence and flooding remote collectors. Also, libsyslog's sendto() on the connected AF_UNIX datagram socket is blocking (no SOCK_NONBLOCK), and this callback is registered with SR_SUBSCR_NO_THREAD, so a full syslogd receive queue would stall the whole confd event loop. A simple token bucket here would cover both.
| * the originator: netopeer2 pushes [nc-sid, username], the CLI and the | ||
| * rpc tool set their originator name to the user. | ||
| */ | ||
| static const char *log_user(sr_session_ctx_t *session) |
There was a problem hiding this comment.
Low: sr_session_get_orig_name() is set by the client itself (copy.c does this too), so any sysrepo client can claim any name, and one claiming netopeer2 without orig data falls through to the literal string netopeer2 as tag. No privilege is gained since app-name is caller-controlled anyway, but the docs should not present the default as an authenticated identity.
|
Didn't find anything when reviewing but i was scared about something like #1643 (comment) so i let claude get a round of this. This i feel is crutial, other it looks fine. |
Agreed, will have a look at it over the weekend, thanks! 🙇♂️ |
Description
Test systems and scripts have had no clean way to leave a mark in a device's log, short of SSH and
logger(1). This adds an RPC that hands a message to the local syslog daemon, exposing the full RFC 5424 header: severity, facility, app-name, msgid, and structured data. Defaults followlogger,user.notice, with app-name defaulting to the calling user. Messages are time stamped on arrival and follow the configured/syslogfiltering and forwarding rules, so the device decides where they end up, not the caller.infix-syslogrevision with thelogRPC, handler builton
syslogp_r()from sysklogd's libsyslog, docs and ChangeLoglog [severity S] [facility F] [msgid M] MESSAGE.The
rpctool now sets the session originator to the calling user andonly splits comma-separated values for leaf-lists, a message with a comma
previously failed
target.log()helper, RESTCONFcall_dict()implemented (itused to return without sending), four tests converted from SSH +
loggersyslog/rpc_logcase verifying header fields and defaultsChecklist
Tick relevant boxes, this PR is-a or has-a: