What an OpenSSH / sshd auth line looks like
The syslog sample below is fed verbatim into the engine to produce every parser on this page.
Jul 3 14:22:15 fw01 sshd[4721]: Failed password for invalid user admin from 203.0.113.45 port 51234 ssh2
Jul 3 14:22:22 fw01 sshd[4725]: Failed password for invalid user oracle from 198.51.100.23 port 50022 ssh2 Detected fields
The engine classified this sample as syslog3164 and consolidated 15 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.
- timestamp : timestamp
- hostname : hostname · literal
- program : literal · literal
- pid : number
- _lit1 : literal · literal
- _lit2 : literal · literal
- _lit3 : literal · literal
- _lit4 : literal · literal
- _lit5 : literal · literal
- user : username
- _lit6 : literal · literal
- srcip : ipv4
- _lit7 : literal · literal
- port1 : port
- _lit8 : literal · literal
Regex (named capture groups)
# sample: Jul 3 14:22:15 fw01 sshd[4721]: Failed password for invalid user admin from 203.0.113.45 port 51234 ssh2
# groups: timestamp=Jul 3 14:22:15, pid=4721, user=admin, srcip=203.0.113.45, port1=51234
^(?<timestamp>[A-Za-z]+ \d+ \d+:\d+:\d+) fw01 sshd\[(?<pid>-?\d+(?:\.\d+)?)\]: Failed password for invalid user (?<user>[A-Za-z0-9._@-]+) from (?<srcip>\d{1,3}(?:\.\d{1,3}){3}) port (?<port1>\d{1,5}) ssh2$ Grok pattern (Logstash / Elastic)
%{SYSLOGTIMESTAMP:timestamp} fw01 sshd\[%{NUMBER:pid}\]: Failed password for invalid user %{USERNAME:user} from %{IPV4:srcip} port %{INT:port1} ssh2 - note constant field "hostname" embedded as literal anchor "fw01" (varying=false)
Wazuh decoder (OS_Regex XML)
<!--
Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
sample: Jul 3 14:22:15 fw01 sshd[4721]: Failed password for invalid user admin from 203.0.113.45 port 51234 ssh2
test with: /var/ossec/bin/wazuh-logtest
-->
<decoder name="sshd-syslog3164">
<program_name>^sshd$</program_name>
</decoder>
<decoder name="sshd-syslog3164">
<parent>sshd-syslog3164</parent>
<regex offset="after_parent">^Failed password for invalid user (\w+) from (\d+.\d+.\d+.\d+) port (\d+) ssh2</regex>
<order>user, srcip, port1</order>
</decoder>
<!-- ============================================================
ALERT RULE (starter) — put this in a RULES file, e.g.
/var/ossec/etc/rules/local_rules.xml. Decoders and rules live
in SEPARATE files. The rule matches the decoder above through
<decoded_as>; set <level> and add <field>/<match> conditions so
it alerts only on the events you care about. Rule ids 100000+
are the user range — change them if they collide with yours.
============================================================ -->
<group name="sshd,">
<rule id="100000" level="3">
<decoded_as>sshd-syslog3164</decoded_as>
<description>sshd: user=$(user) srcip=$(srcip)</description>
</rule>
<!-- Example — a higher-level alert gated on one field (uncomment and edit):
<rule id="100001" level="10">
<if_sid>100000</if_sid>
<field name="user">^admin$</field>
<description>sshd: privileged-account activity from $(user)</description>
</rule>
-->
</group>
- note syslog header fields handled by Wazuh pre-decoding (not re-parsed): timestamp, hostname, program, pid
- note parent matches by <program_name> (1 program(s) seen in the samples) — extend the alternation for other programs
- note added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
- note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest
Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character.
Test Wazuh OS_Regex patterns →
rsyslog template / liblognorm rulebase
# rsyslog template — put in /etc/rsyslog.d/sshd.conf
# Emits the detected RFC 3164 header fields plus the raw
# message body as JSON-ish text using standard rsyslog properties.
# Bind the template to an action, e.g.:
# action(type="omfile" file="/var/log/sshd.log" template="sshd")
# A literal "%" inside a template string must be escaped as "%%".
template(name="sshd" type="string" string="{\"timestamp\":\"%timereported%\",\"host\":\"%hostname%\",\"program\":\"%programname%\",\"procid\":\"%procid%\",\"msg\":\"%msg:::json%\"}\n")
# --- mmnormalize rulebase for the message body ----------------------
# rsyslog properties cover only the syslog header. To extract the fields
# inside the message body, save the lines below (starting at 'version=2',
# which must be the VERY FIRST line of the file) as /etc/rsyslog.d/sshd.rb
# and load them with:
# module(load="mmnormalize")
# action(type="mmnormalize" rulebase="/etc/rsyslog.d/sshd.rb")
version=2
rule=sshd_msg: Failed password for invalid user %user:word% from %srcip:ipv4% port %port1:number% ssh2
- note header property mapping: timestamp -> %timereported%, hostname -> %hostname%, program -> %programname%, pid -> %procid%
- note message-body fields cannot be extracted by rsyslog properties alone — an mmnormalize (liblognorm v2) rulebase for the msg part is appended below the template; save it as its own .rb file
- note dropped header separator "]:" before the first message field (it belongs to the syslog tag, not the msg property)
- note chosen parser types: user=word, srcip=ipv4, port1=number
Splunk
# props.conf (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = (?<timestamp>[A-Za-z]+ \d+ \d+:\d+:\d+) fw01 sshd\[(?<pid>-?\d+(?:\.\d+)?)\]: Failed password for invalid user (?<user>[A-Za-z0-9._@-]+) from (?<srcip>\d{1,3}(?:\.\d{1,3}){3}) port (?<port1>\d{1,5}) ssh2
# Quick search-time test in SPL:
# | rex field=_raw "(?<timestamp>[A-Za-z]+ \\d+ \\d+:\\d+:\\d+) fw01 sshd\\[(?<pid>-?\\d+(?:\\.\\d+)?)\\]: Failed password for invalid user (?<user>[A-Za-z0-9._@-]+) from (?<srcip>\\d{1,3}(?:\\.\\d{1,3}){3}) port (?<port1>\\d{1,5}) ssh2" - note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
- note a timestamp field was detected: this EXTRACT only makes it a searchable field. To set the event _time at index time, add TIME_PREFIX and TIME_FORMAT to this props.conf stanza (TIME_FORMAT uses Splunk strptime, e.g. %Y-%m-%dT%H:%M:%S) — this generator does not guess the strptime format.
ES ingest
PUT _ingest/pipeline/sshd
{
"description": "LogForge-generated ingest pipeline for sshd",
"processors": [
{
"grok": {
"field": "message",
"patterns": [
"%{SYSLOGTIMESTAMP:timestamp} fw01 sshd\\[%{NUMBER:pid}\\]: Failed password for invalid user %{USERNAME:user} from %{IPV4:srcip} port %{INT:port1} ssh2"
]
}
}
]
} - note grok: constant field "hostname" embedded as literal anchor "fw01" (varying=false)
- note test in Kibana Dev Tools with: POST _ingest/pipeline/sshd/_simulate (supply a docs[] array whose _source.message holds a sample line)
Graylog
# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "sshd-parse"
when
has_field("message")
then
let gp = grok(pattern: "%{SYSLOGTIMESTAMP:timestamp} fw01 sshd\\[%{NUMBER:pid}\\]: Failed password for invalid user %{USERNAME:user} from %{IPV4:srcip} port %{INT:port1} ssh2", value: to_string($message.message), only_named_captures: true);
set_fields(gp);
end
# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
"extractors": [
{
"title": "sshd",
"extractor_type": "grok",
"converters": [],
"order": 0,
"cursor_strategy": "copy",
"source_field": "message",
"target_field": "",
"extractor_config": {
"grok_pattern": "%{SYSLOGTIMESTAMP:timestamp} fw01 sshd\\[%{NUMBER:pid}\\]: Failed password for invalid user %{USERNAME:user} from %{IPV4:srcip} port %{INT:port1} ssh2",
"named_captures_only": true
},
"condition_type": "none",
"condition_value": ""
}
],
"version": "5.0.0"
} - note grok: constant field "hostname" embedded as literal anchor "fw01" (varying=false)
- note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI
Datadog
logforge_rule %{date("MMM dd HH:mm:ss"):timestamp} fw01 sshd\[%{number:pid}\]: Failed password for invalid user %{notSpace:user} from %{ipv4:srcip} port %{integer:port1} ssh2 - note emitted rule name is "logforge_rule"; rename it to match your "sshd" convention if desired
- note constant field "hostname" embedded as literal anchor "fw01" (varying=false)
- note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.
Fluent Bit
[PARSER]
Name sshd
Format regex
Regex ^(?<timestamp>[A-Za-z]+ \d+ \d+:\d+:\d+) fw01 sshd\[(?<pid>-?\d+(?:\.\d+)?)\]: Failed password for invalid user (?<user>[A-Za-z0-9._@-]+) from (?<srcip>\d{1,3}(?:\.\d{1,3}){3}) port (?<port1>\d{1,5}) ssh2$
Time_Key timestamp
Time_Format %b %d %H:%M:%S
# Fluentd <parse> block:
# <parse>
# @type regexp
# expression /^(?<timestamp>[A-Za-z]+ \d+ \d+:\d+:\d+) fw01 sshd\[(?<pid>-?\d+(?:\.\d+)?)\]: Failed password for invalid user (?<user>[A-Za-z0-9._@-]+) from (?<srcip>\d{1,3}(?:\.\d{1,3}){3}) port (?<port1>\d{1,5}) ssh2$/
# time_key timestamp
# time_format %b %d %H:%M:%S
# </parse>
- note Time_Key set to "timestamp"; Time_Format "%b %d %H:%M:%S" is a best-effort strptime derived from the sample shape — verify it against your data (Fluent Bit uses %L for fractional seconds and %z for numeric offsets)
Vector
[transforms.sshd_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'(?P<timestamp>[A-Za-z]+ \d+ \d+:\d+:\d+) fw01 sshd\[(?P<pid>-?\d+(?:\.\d+)?)\]: Failed password for invalid user (?P<user>[A-Za-z0-9._@-]+) from (?P<srcip>\d{1,3}(?:\.\d{1,3}){3}) port (?P<port1>\d{1,5}) ssh2')
''' Loki
# promtail pipeline for "sshd" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
# scrape_configs:
# - job_name: sshd
# pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
- regex:
expression: '^(?P<timestamp>[A-Za-z]+ \d+ \d+:\d+:\d+) fw01 sshd\[(?P<pid>-?\d+(?:\.\d+)?)\]: Failed password for invalid user (?P<user>[A-Za-z0-9._@-]+) from (?P<srcip>\d{1,3}(?:\.\d{1,3}){3}) port (?P<port1>\d{1,5}) ssh2$'
- note no low-cardinality field found to promote to a Loki label — omitted the `- labels:` stage; every captured field stays in the extracted map for later stages
- note left in the extracted map (NOT promoted to labels — high cardinality would explode Loki streams): timestamp, pid, user, srcip, port1
syslog-ng
parser p_sshd {
regexp-parser(
prefix(".sshd.")
patterns("(?<timestamp>[A-Za-z]+ \\d+ \\d+:\\d+:\\d+) fw01 sshd\\[(?<pid>-?\\d+(?:\\.\\d+)?)\\]: Failed password for invalid user (?<user>[A-Za-z0-9._@-]+) from (?<srcip>\\d{1,3}(?:\\.\\d{1,3}){3}) port (?<port1>\\d{1,5}) ssh2")
);
}; - note captured fields are stored as name-value pairs under the prefix ".sshd." (e.g. a group (?<srcip>…) becomes ".sshd.srcip")
FAQ
- What does "invalid user" mean in an sshd Failed password line?
- It means the username being tried does not exist on the system. sshd inserts the phrase 'invalid user' before the name (Failed password for invalid user admin from …) to distinguish it from a failure against a real account. A flood of invalid-user failures is the classic signature of untargeted SSH brute-forcing, whereas repeated failures for a real account like root suggest a targeted guess.
- How do I detect an SSH brute-force attack from these logs?
- Count 'Failed password' events grouped by source IP over a short window; a single address producing dozens of failures — especially against invalid users — is brute-forcing. The higher-fidelity signal is a source that shows many failures followed by an 'Accepted password' or 'Accepted publickey': that is a successful break-in and should page, not just alert.
- Why is there no year in the sshd log timestamp?
- Because it uses the RFC 3164 (BSD syslog) header, which only records month, day, and time — never the year. Parsers must supply the year from context (usually the current year), and take care around the year boundary where a December log read in January can be misdated. RFC 5424 syslog and journald both fix this by using full ISO 8601 timestamps.
- Which fields should I extract from an sshd auth log for a SIEM?
- The outcome (Accepted vs Failed), the authentication method (password vs publickey), the username, the source IP and source port, and on successful publickey logins the key type and fingerprint. Those five to six fields cover brute-force detection, credential-stuffing (distinct users per source), and attribution of a session to a specific key.
Try it on your own OpenSSH / sshd auth lines
Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.
Open this sample in LogForge →