I installed syslog-ng to use on my desktop (Gentoo 64bit, upgraded to systemd i.e. was OpenRC before, with Openbox and Slim only) with my normal user to log all commands I type in the shell (bash first, then eventually zsh). I've explored different solutions, and different ways of setting this up, old and new and often this is achieved using the .bash_history file. I'm trying to implement this solution from a few years ago, with reliance on the companion trap. First I've modified .bashrc and set some history variables because the solution relies on formatting the history data, then making sure it is saved to its file, then pushing it to the log messaging system with logger in a function called in the shell environment. So first the variables:

export HISTFILE=$HOME/.bash_history
export HISTFILESIZE=2000
export HISTSIZE=1000
export HISTTIMEFORMAT="%a %b %Y %T %z "

typeset -r HISTCONTROL
typeset -r HISTFILE
typeset -r HISTIGNORE
typeset -r HISTSIZE

shopt -s cmdhist
shopt -s histappend

PROMPT_COMMAND="history -a"

ex. history command output with timestamps
860  Tue Jan 2014 10:33:50 -0900 exit
861  Tue Jan 2014 10:33:56 -0900 ls
862  Tue Jan 2014 10:33:58 -0900 history

Then, as explained in the linked article, you must add this trap which uses logger in .bashrc (there is reference to /etc/profile, but here I want this for my regular user only and ~/.profile is not sourced by something like lxterminal):

function log2syslog
   declare command
   command=$(fc -ln -0)
   logger -p local1.notice -t bash -i -- $USER : $command
trap log2syslog DEBUG

A single long hyphen was (mistakenly?) used in the original doc, followed by a space and $USER.

I've replaced my original syslog-ng configuration file. I've tried the suggested config from Arch, but after some warnings, I've configured it like so explained for Gentoo which is what the Arch doc is based on anyway:

@version: 3.4
options {

        # The default action of syslog-ng is to log a STATS line
        # to the file every 10 minutes.  That's pretty ugly after a while.
        # Change it to every 12 hours so you get a nice daily update of
        # how many messages syslog-ng missed (0).

source src {
    unix-dgram("/dev/log" max-connections(256));

source kernsrc { file("/proc/kmsg"); };

# define destinations
destination authlog { file("/var/log/auth.log"); };
destination syslog { file("/var/log/syslog"); };
destination cron { file("/var/log/cron.log"); };
destination daemon { file("/var/log/daemon.log"); };
destination kern { file("/var/log/kern.log"); };
destination lpr { file("/var/log/lpr.log"); };
destination user { file("/var/log/user.log"); };
destination mail { file("/var/log/mail.log"); };

destination mailinfo { file("/var/log/mail.info"); };
destination mailwarn { file("/var/log/mail.warn"); };
destination mailerr { file("/var/log/mail.err"); };

destination newscrit { file("/var/log/news/news.crit"); };
destination newserr { file("/var/log/news/news.err"); };
destination newsnotice { file("/var/log/news/news.notice"); };

destination debug { file("/var/log/debug"); };
destination messages { file("/var/log/messages"); };
destination console { usertty("root"); };

# By default messages are logged to tty12...
destination console_all { file("/dev/tty12"); };

# ...if you intend to use /dev/console for programs like xconsole
# you can comment out the destination line above that references /dev/tty12
# and uncomment the line below.
#destination console_all { file("/dev/console"); };

# create filters
filter f_authpriv { facility(auth, authpriv); };
filter f_syslog { not facility(authpriv, mail); };
filter f_cron { facility(cron); };
filter f_daemon { facility(daemon); };
filter f_kern { facility(kern); };
filter f_lpr { facility(lpr); };
filter f_mail { facility(mail); };
filter f_user { facility(user); };
filter f_debug { not facility(auth, authpriv, news, mail); };
filter f_messages { level(info..warn)
        and not facility(auth, authpriv, mail, news); };
filter f_emergency { level(emerg); };

filter f_info { level(info); };
filter f_notice { level(notice); };
filter f_warn { level(warn); };
filter f_crit { level(crit); };
filter f_err { level(err); };
filter f_failed { message("failed"); };
filter f_denied { message("denied"); };

# connect filter and destination
log { source(src); filter(f_authpriv); destination(authlog); };
log { source(src); filter(f_syslog); destination(syslog); };
log { source(src); filter(f_cron); destination(cron); };
log { source(src); filter(f_daemon); destination(daemon); };
log { source(kernsrc); filter(f_kern); destination(kern); };
log { source(src); filter(f_lpr); destination(lpr); };
log { source(src); filter(f_mail); destination(mail); };
log { source(src); filter(f_user); destination(user); };
log { source(src); filter(f_mail); filter(f_info); destination(mailinfo); };
log { source(src); filter(f_mail); filter(f_warn); destination(mailwarn); };
log { source(src); filter(f_mail); filter(f_err); destination(mailerr); };

log { source(src); filter(f_debug); destination(debug); };
log { source(src); filter(f_messages); destination(messages); };
log { source(src); filter(f_emergency); destination(console); };

# default log
log { source(src); destination(console_all); };

Of note is the comment from Arch wiki about the unix-stream reference misbehaving and prohibiting loading syslog-ng at startup. Changing the reference to unix-dgram takes care of that is basically the only change from the model used, except for providing a version number on the first line. After that you can do systemctl enable syslog-ng to have that available at boot.

So it is up and running manually here:

# systemctl status syslog-ng
syslog-ng.service - System Logger Daemon
   Loaded: loaded (/usr/lib64/systemd/system/syslog-ng.service; disabled)
   Active: active (running) since Tue 2014-01-28 20:23:36 EST; 1s ago
     Docs: man:syslog-ng(8)
 Main PID: 9238 (syslog-ng)
   CGroup: /system.slice/syslog-ng.service
           \u2514\u25009238 /usr/sbin/syslog-ng -F

Jan 28 20:23:36 gentoouser3x86_64 systemd[1]: Starting System Logger Daemon...
Jan 28 20:23:36 gentoouser3x86_64 systemd[1]: Started System Logger Daemon.

And I get the desired basic ouput in /var/log/messages:

Jan 28 20:42:00 gentoouser3x86_64 bash[9878]: myuser : shopt
Jan 28 20:42:04 gentoouser3x86_64 bash[9880]: myuser : su -
Jan 29 03:30:58 gentoouser3x86_64 bash[4386]: myuser : ls
Jan 29 03:30:58 gentoouser3x86_64 bash[4389]: myuser : ls  <--- duplicate
Jan 29 03:30:58 gentoouser3x86_64 bash[4391]: myuser : ls  <--- entries
Jan 29 04:36:31 gentoouser3x86_64 bash[4491]: myuser : cat .bashrc
Jan 29 04:37:14 gentoouser3x86_64 bash[4495]: myuser : cat .bashrc  <---
Jan 29 04:37:14 gentoouser3x86_64 bash[4497]: myuser : cat .bashrc  <---
Jan 29 04:37:35 gentoouser3x86_64 bash[4500]: myuser : nedit .bashrc
Jan 29 04:37:35 gentoouser3x86_64 bash[4503]: myuser : nedit .bashrc  <---
Jan 29 04:37:35 gentoouser3x86_64 bash[4505]: myuser : nedit .bashrc  <---

Or, if I disable syslog-ng with systemctl stop syslog-ng, I get the very same output from the journald binary log using journalctl -f (or journalctl -f -o verbose for the details) because systemd "takes over" in that case. Restarting syslog-ng and/or its socket reclaims the output immediately and sends it to its assorted files specified in its configuration.


  • Whether I use syslog-ng or journald, I get many duplicate lines in the logs whereas the commands were only executed once. Listing the contents of a directory for instance may show ls 2-3 times in the logs when I use many terminal windows. In particular, pressing enter in the CLI seems to echo the the last command in the log. Why? (Is it because the variable in the trap is still set to the last line of the history file? If so how can this be remedied?)

The main source link indicates that since version 4.1, bash can write to syslog directly... the changelog says:

"There is a new configuration option (in config-top.h) that forces bash to forward all history entries to syslog."

  • So is the trap function used here still useful or is it obsolete? Is the a more modern/elegant way of doing this? Is that >4.1 option exposed somewhere or do you need to recompile bash to do that? What is it?
  • Aside from built-in options that are native to bash, can we expect implementing a similar solution for zsh? Or again, is there a better and more integrated way of doing this?
  • Is there lots of overhead generated from sending all the commands to the logs, and are journald and syslog-ng equal in this respect?
  • your title is still very confuse. In my opinion you should try to tease people, with that title you'r putting too much infos. but that only my personal opinion.
    – Kiwy
    Commented Jan 29, 2014 at 13:08
  • Maybe something like: "Getting duplicate entry using logger and syslog-ng to log every shell command" would be clearer ;-)
    – Kiwy
    Commented Jan 29, 2014 at 13:15
  • @Kiwy I tried again. I'd like the way of doing it exposed in the Q title because it's a lot about the way to do it. Thanks!
    – user44370
    Commented Jan 29, 2014 at 13:26
  • 1
    Server problems: missing quotes, assume commands don't contain newline characters. More importantly, the DEBUG trap is executed for each command while fc is to report the command lines (one per prompt). Use $BASH_COMMAND to get the current command in a DEBUG trap or use fc in an action in $PROMPT_COMMAND. Commented Jan 29, 2014 at 13:45
  • @StephaneChazelas Ok, PROMPT_COMMAND=$(history -a) and command=$BASH_COMMAND sanitize the output and seemingly relieve it of duplicate lines!! That's a "start". Thank you for the insight! Even better, more info is echoed in the log i.e. for ls I get the aliased stuff like --color=auto etc!!
    – user44370
    Commented Jan 29, 2014 at 14:45

3 Answers 3


You have a lot going on there..... My best answer to this is to explain simply how I have seen session logging done in the past. Hopefully that will give you some options to explore.

  1. As you have already mentioned, pulling the bash history from the user accounts. This only works after the session has ended. Not really the best option but it's easy and reliable.
  2. Using a virtual terminal such as the screen command in Linux. This is not very robust as it starts on the user login however if they know it's being logged you can still kill the service. This works well in an end-user scenario. End users generally are trapped in a specified area anyway and don't have the knowledge to get around this.
  3. Pam_tty_audit module & aureport --tty This is a tool that allows you to specify which users get logged and allow you to specify the storage location of said logs... as always keep the logs off of the host server. I have the session logs on our SFTP server being copied off to a central logging server and a local cronjob moving them to a non shared location for archive.

This is built in for RedHat and Fedora however you can install it on Debian and Ubuntu. It's part of the auditd package I believe. Here is some documentation on auditd and the required configuration changes to pam (in /etc/pam.d/system-auth), specifying a single user here(root):

session required pam_tty_audit.so disable=* enable=root

Example output of aureport --tty:

TTY Report
# date time event auid term sess comm data
1. 1/29/2014 00:08:52 122249 0000 ? 4686960298 bash "ls -la",<ret> 
  • Here is some additional information: > doc.opensuse.org/products/draft/SLES/SLES-security_sd_draft/… < This might be helpful.
    – DaffyDuc
    Commented Jan 29, 2014 at 15:48
  • Here is some more specific instructions to get the basics configured. this is for redhat > jaredrobinson.com/blog/linux-tty-auditing
    – DaffyDuc
    Commented Jan 29, 2014 at 17:15
  • Your not trying to use both config lines I hope. I know when I was originally configuring this I copied and pasted the 2 lines into my config and promptly broke our test server.... NOt sure how to help you without an excruciating amount of detail and a huge comment string... maybe try to ask a narrower question here. see if someone can help you with that specific problem.
    – DaffyDuc
    Commented Jan 29, 2014 at 18:10
  • pam tty audit and reporting is spot on! To use it on Gentoo, I needed to add some local use flags to package.use (dev-util/perf audit, sys-apps/systemd audit, and sys-libs/pam audit). Note in (1), bash_history based solutions need not wait for the end of a session because history -a appends immediately what is new since the session started. Actually it is the pam_tty_audit module(3) which waits for the end of a session i.e. I needed to exit my terminal emulator for the new stuff to appear with aureport -tty. Indeed, each numbered line in the report represents activity for a bash session.
    – user44370
    Commented Jan 30, 2014 at 10:48
  • How does auditd integrate with journald? Do their functions overlap Commented Aug 15, 2014 at 11:04

This is solution which takes care of the first question, as well as introduces the use of auditd interactively, outside of the pam_tty module solution provided in the other answer.


First, as explained by a contributor, there might be syntax issues with the original setup and there is a better way to do this using the $BASH_COMMAND variable:

The command currently being executed or about to be executed, unless the shell is executing a command as the result of a trap, in which case it is the command executing at the time of the trap.

Updating the original prompt_command reference and the function like so works:

PROMPT_COMMAND=$(history -a)

function log2syslog
   declare command
   logger -p local1.notice -t bash -i -- $USER : $command

trap log2syslog DEBUG

So the new history lines are written to the bash_history file every time because of PROMPT_COMMAND, and since $BASH_COMMAND is in the trap, the command typed on the cli is the command being executed. If I remove the history -a, I can see my PS1 being echoed. Works perfectly and removes all the duplicate lines. The output includes also alias expansion for some reason.

It is also possible to do this without the trap using PROMPT_COMMAND only, like so:

PROMPT_COMMAND='history -w; history -a; history -r; command=$(fc -ln 0); logger -p local1.notice -t bash -i -- $USER : $command'

It doesn't show alias expansion, but it has a small defect I can't correct: if you just press enter with nothing else on the line, it outputs the last command to logs. You can't merge the arwn history options. We write the history we have to file, then write the appended history since the beginning of the session, then we read it back, and then we look for the last line.


With zsh we can use the precmd builtin function, similar to P_C, like so, with one shell specific shell option, all in .zshrc:

 setopt incappendhistory

 precmd () {
    command="$(fc -n -e - -l -1)"
    logger -p local1.notice -t zsh -i "$USER : $command"

And that's it!


Audit is an auditing package containing a daemon with plugins and reporting facilities. It uses a rules based approach (see security oriented audit.rules example here) to trap and log events. Install the package then make sure you have this in /etc/conf.d/auditd:


You can also review many other options in /etc/audit/auditd.conf, including the log path(/var/log/audit/audit.log by default).

The framework also includes a dispatcher with plugins, including one which has the ability to write directly to syslog if so desired, for convenience. You must first enable it in /etc/audisp/plugins.d/sysconf by enabling active:

active = yes
direction = out
path = builtin_syslog
type = builtin 
args = LOG_INFO
format = string

You will also find the facilities (audisp-remote) in the dispatcher for remote logging but this need not be set to enable the dispatching to syslog here. If you enable the syslog plugin and journald is used instead of syslog, journald will show the output as well as /var/log/audit/audit.log (default setup). Once the setup is complete, you can start the daemon it with auditd -s enable. From the logs:

Started dispatcher: /sbin/audispd pid: 3869
audispd[3869]: priority_boost_parser called with: 4
audispd[3869]: max_restarts_parser called with: 10
audispd[3869]: syslog plugin initialized
audispd[3869]: audispd initialized with q_depth=120 and 1 active plugins
audispd[3869]: node=gentoouser3x86_64 type=DAEMON_START msg=audit(1391089266.449:1498): auditd start, ver=2.1.3 format=raw kernel=3.10.2...res=success
auditd[3867]: Init complete, auditd 2.1.3 listening for events (startup state enable)

you can easily check the status too with auditctl -s:
AUDIT_STATUS: enabled=1 flag=1 pid=3867 rate_limit=0 backlog_limit=64 lost=21 backlog=0

Interactive use doesn't require it, but if you don't have a valid auditd.service unit to manage with systemctl for starting at boot, you may try instead simply including audit=1 as a kernel boot parameter as often this has been implemented at the kernel level.

The framework is now up and running and you can selectively trap events interactively on the cli using auditctl. Tons of options are available. In particular we can monitor EXECVE system calls and see the commands used in the shell as arguments here:

# auditctl -a exit,always -F arch=b64 -S execve
(audit.log output)
type=EXECVE msg=audit(1391090877.859:98): argc=3 a0="ls" a1="--color=auto" a2="-la"
type=CWD msg=audit(1391090877.859:98):  cwd="/root"
type=PATH msg=audit(1391090877.859:98): item=0 name="/bin/ls" inode=4194395 dev=08:34 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL
type=PATH msg=audit(1391090877.859:98): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL
type=SYSCALL msg=audit(1391090887.955:99): arch=c000003e syscall=59 success=yes exit=0 a0=1de21f0 a1=1de10c0 a2=1db7960 a3=7fff3691a390 items=2 ppid=3994 pid=4006 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 ses=1 tty=pts2 comm="clear" exe="/usr/bin/clear" key=(null)
type=EXECVE msg=audit(1391090887.955:99): argc=1 a0="clear"
type=CWD msg=audit(1391090887.955:99):  cwd="/root"
type=PATH msg=audit(1391090887.955:99): item=0 name="/usr/bin/clear" inode=1966198 dev=08:34 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL

Here's a sample showing both outputs from our trap function and
auditd appearing in our log using journald (which also shows our logger command in the 'trap'):

bash[4410]: myuser : ls --color=auto
audispd[3869]: node=gentoomyuser3x86_64 type=SYSCALL msg=audit(1391096772.067:120): arch=c000003e syscall=59 success=yes exit=0 a0=8e5a10 a1=8e4a60 a...
audispd[3869]: node=gentoomyuser3x86_64 type=EXECVE msg=audit(1391096772.067:120): argc=2 a0="ls" a1="--color=auto"
audispd[3869]: node=gentoomyuser3x86_64 type=CWD msg=audit(1391096772.067:120):  cwd="/home/myuser"
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096772.067:120): item=0 name="/bin/ls" inode=4194395 dev=08:34 mode=010075...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096772.067:120): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 o...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=EOE msg=audit(1391096772.067:120):
audispd[3869]: node=gentoomyuser3x86_64 type=SYSCALL msg=audit(1391096807.548:121): arch=c000003e syscall=59 success=yes exit=0 a0=8e5c50 a1=8e6430 a...
audispd[3869]: node=gentoomyuser3x86_64 type=EXECVE msg=audit(1391096807.548:121): argc=10 a0="logger" a1="-p" a2="local1.notice" a3="-t" ..." a9="date"
audispd[3869]: node=gentoomyuser3x86_64 type=CWD msg=audit(1391096807.548:121):  cwd="/home/myuser"
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096807.548:121): item=0 name="/usr/bin/logger" inode=1966477 dev=08:34 mod...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096807.548:121): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 o...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=EOE msg=audit(1391096807.548:121):
bash[4415]: myuser : date
audispd[3869]: node=gentoomyuser3x86_64 type=SYSCALL msg=audit(1391096807.549:122): arch=c000003e syscall=59 success=yes exit=0 a0=8e5f40 a1=8e5cd0 a...
audispd[3869]: node=gentoomyuser3x86_64 type=EXECVE msg=audit(1391096807.549:122): argc=1 a0="date"
audispd[3869]: node=gentoomyuser3x86_64 type=CWD msg=audit(1391096807.549:122):  cwd="/home/myuser"
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096807.549:122): item=0 name="/bin/date" inode=4194318 dev=08:34 mode=0100...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096807.549:122): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 o...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=EOE msg=audit(1391096807.549:122):
audispd[3869]: node=gentoomyuser3x86_64 type=SYSCALL msg=audit(1391096838.004:123): arch=c000003e syscall=59 success=yes exit=0 a0=8e6c60 a1=8e6d00 a...
audispd[3869]: node=gentoomyuser3x86_64 type=EXECVE msg=audit(1391096838.004:123): argc=21 a0="logger" a1="-p" a2="local1.notice" a3="-t" a4="bash" a...
audispd[3869]: node=gentoomyuser3x86_64 type=CWD msg=audit(1391096838.004:123):  cwd="/home/myuser"
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096838.004:123): item=0 name="/usr/bin/logger" inode=1966477 dev=08:34 mod...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096838.004:123): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 o...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=EOE msg=audit(1391096838.004:123):
bash[4417]: myuser : aafire -width 82 -height 25 -gamma 1 -floyd_steinberg -font mda14 -driver curses
audispd[3869]: node=gentoomyuser3x86_64 type=SYSCALL msg=audit(1391096838.006:124): arch=c000003e syscall=59 success=yes exit=0 a0=8e6c60 a1=8c3880 a...
audispd[3869]: node=gentoomyuser3x86_64 type=EXECVE msg=audit(1391096838.006:124): argc=12 a0="aafire" a1="-width" a2="82" a3="-height" a4...11="curses"
audispd[3869]: node=gentoomyuser3x86_64 type=CWD msg=audit(1391096838.006:124):  cwd="/home/myuser"
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096838.006:124): item=0 name="/usr/bin/aafire" inode=1594727 dev=08:34 mod...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096838.006:124): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 o...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=EOE msg=audit(1391096838.006:124):
audispd[3869]: node=gentoomyuser3x86_64 type=SYSCALL msg=audit(1391096852.816:125): arch=c000003e syscall=59 success=yes exit=0 a0=8e5870 a1=8e5b20 a...
audispd[3869]: node=gentoomyuser3x86_64 type=EXECVE msg=audit(1391096852.816:125): argc=11 a0="logger" a1="-p" a2="local1.notice" a3="-t" ...su" a10="-"
audispd[3869]: node=gentoomyuser3x86_64 type=CWD msg=audit(1391096852.816:125):  cwd="/home/myuser"
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096852.816:125): item=0 name="/usr/bin/logger" inode=1966477 dev=08:34 mod...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=PATH msg=audit(1391096852.816:125): item=1 name=(null) inode=1613135 dev=08:34 mode=0100755 o...type=NORMAL
audispd[3869]: node=gentoomyuser3x86_64 type=EOE msg=audit(139109

...terminate outputting by simply removing the active rules with auditctl -D or stop the entire auditing with auditd -s disable or auditctl -e 0. We don't get to see all character sequences typed to the terminal using this rule but we log all commands.


Add the following line in /etc/profiles or ~/.bashrc

 PROMPT_COMMAND='history -a >(tee -a ~/.bash_history | logger -t "$USER[$$] $SSH_CONNECTION")'

this will send the command along with ssh user/ip to /var/log/messages or /var/log/syslog

You must log in to answer this question.