Sudo 1.9: using the new Audit API from Python
Version 1.9 of sudo introduced a new API to access audit information. This is not a user-visible feature. In other words, you cannot use it directly from the sudoers
file. It is an API, meaning that you can access audit information from plugins, including ones written in Python. You can use it in many different ways, like sending events from sudo directly to Elasticsearch or LaaS when something interesting happens. You can also use it for debugging and print otherwise difficult to access information to the screen in whatever format you like.
In this blog you will find a simple Python plugin which displays information on the terminal when a command is run. It is derived from a more complex example that is available as part of the sudo package called example_audit_plugin.py
.
In order to use the sudo Python bindings, you need to have sudo version 1.9 with Python support enabled. This is available, for example, in openSUSE Tumbleweed, although most distributions use an earlier version. For many distros and UNIX variants, there are ready to use packages on the sudo website. For others, you can easily compile it yourself based on information from one of my earlier blogs.
Python plugins must be accessible only by root. For simplicity, I have stored it in the home directory of the root user, but with the right amount of chown
and chmod
magic you can store it anywhere on the file system. Here is the content of /root/sudo_audit.py:
import sudo
class SudoAuditPlugin(sudo.Plugin):
def __init__(self, plugin_options, user_info, **kwargs):
# For loading multiple times, an optional "Id" can be specified
# as argument to identify the log lines
plugin_id = sudo.options_as_dict(plugin_options).get("Id", "")
self._log_line_prefix = "(AUDIT{}) ".format(plugin_id)
user_info_dict = sudo.options_as_dict(user_info)
# print(user_info_dict)
user = user_info_dict.get("user", "???")
uid = user_info_dict.get("uid", "???")
self._log("-- Started by user {} ({}) -- ".format(user, uid))
def accept(self, plugin_name, plugin_type,
command_info, run_argv, run_envp) -> int:
info = sudo.options_as_dict(command_info)
# print(info)
cmd = list(run_argv)
cmd[0] = info.get("command")
self._log("Accepted command: {}".format(" ".join(cmd)))
self._log(" Environment: " + " ".join(run_envp))
def _log(self, string):
# For the example, we just log to output (this could be a file)
sudo.log_info(self._log_line_prefix, string)
It starts by importing the sudo module. You won’t find this in the file system, it is provided by the sudo Python plugin itself. Next, we create a class based on the sudo.Plugin
class. You can name it whatever you want as long as you use that name in the sudo.conf
file.
There are no mandatory methods when accessing the Audit Plugin API. In this sample code we define three methods. _log()
is a wrapper around the sudo.log_info()
method which prints log messages to the terminal. The other two are called when sudo is started and when a command is accepted by the policy. Let’s look at them in more detail!
The __init__()
method is called when sudo is started. You can pass options from sudo.conf
to the Python plugin. As multiple Python audit plugins can be loaded, the optional Id
parameter can be used to identify in the logs which plugin it belongs to. We use this string when defining the log prefix string on the next line, which is later used by the _log()
method. Next, we turn user_info
into a dict and use it to set the user
and uid
variables. The last line of the method prints these values to the screen. There is also a line that is commented out which would print the values stored in user_info_dict
to the screen.
The accept()
method is called by sudo when a command is accepted by the policy or an approval plugin. We start by turning the value of command_info
into a dict, and use this information to get the command name. On the last two lines, we print the command with its parameters and the environment in which the command is run. The commented out line would print all values from command_info
.
The plugin has to be enabled in the sudo.conf
file. Assuming that you used the same path and file name as I did, you should append the following lines to it:
Plugin python_audit python_plugin.so \
ModulePath=/root/sudo_audit.py \
ClassName=SudoAuditPlugin Id=666
The final parameter is the optional ID passed to __init__()
, which can be used in the prefix of log messages.
Here is how running ls /
through sudo looks with the above Python plugin enabled on CentOS 7:
[czanik@centos7sudo ~]$ sudo ls /
(AUDIT666) -- Started by user czanik (1000) --
[sudo] password for czanik:
(AUDIT666) Accepted command: /bin/ls /
(AUDIT666) Environment: HOSTNAME=centos7sudo.localdomain TERM=xterm-256color LS_COLORS=rs=0:di=38;5;27:ln=38;5;51:mh=44;38;5;15:pi=40;38;5;11:so=38;5;13:do=38;5;5:bd=48;5;232;38;5;11:cd=48;5;232;38;5;3:or=48;5;232;38;5;9:mi=05;48;5;232;38;5;15:su=48;5;196;38;5;15:sg=48;5;11;38;5;16:ca=48;5;196;38;5;226:tw=48;5;10;38;5;16:ow=48;5;10;38;5;21:st=48;5;21;38;5;15:ex=38;5;34:*.tar=38;5;9:*.tgz=38;5;9:*.arc=38;5;9:*.arj=38;5;9:*.taz=38;5;9:*.lha=38;5;9:*.lz4=38;5;9:*.lzh=38;5;9:*.lzma=38;5;9:*.tlz=38;5;9:*.txz=38;5;9:*.tzo=38;5;9:*.t7z=38;5;9:*.zip=38;5;9:*.z=38;5;9:*.Z=38;5;9:*.dz=38;5;9:*.gz=38;5;9:*.lrz=38;5;9:*.lz=38;5;9:*.lzo=38;5;9:*.xz=38;5;9:*.bz2=38;5;9:*.bz=38;5;9:*.tbz=38;5;9:*.tbz2=38;5;9:*.tz=38;5;9:*.deb=38;5;9:*.rpm=38;5;9:*.jar=38;5;9:*.war=38;5;9:*.ear=38;5;9:*.sar=38;5;9:*.rar=38;5;9:*.alz=38;5;9:*.ace=38;5;9:*.zoo=38;5;9:*.cpio=38;5;9:*.7z=38;5;9:*.rz=38;5;9:*.cab=38;5;9:*.jpg=38;5;13:*.jpeg=38;5;13:*.gif=38;5;13:*.bmp=38;5;13:*.pbm=38;5;13:*.pgm=38;5;13:*.ppm=38;5;13:*.tga=38;5;13:*.xbm=38;5;13:*.xpm=38;5;13:*.tif=38;5;13:*.tiff=38;5;13:*.png=38;5;13:*.svg=38;5;13:*.svgz=38;5;13:*.mng=38;5;13:*.pcx=38;5;13:*.mov=38;5;13:*.mpg=38;5;13:*.mpeg=38;5;13:*.m2v=38;5;13:*.mkv=38;5;13:*.webm=38;5;13:*.ogm=38;5;13:*.mp4=38;5;13:*.m4v=38;5;13:*.mp4v=38;5;13:*.vob=38;5;13:*.qt=38;5;13:*.nuv=38;5;13:*.wmv=38;5;13:*.asf=38;5;13:*.rm=38;5;13:*.rmvb=38;5;13:*.flc=38;5;13:*.avi=38;5;13:*.fli=38;5;13:*.flv=38;5;13:*.gl=38;5;13:*.dl=38;5;13:*.xcf=38;5;13:*.xwd=38;5;13:*.yuv=38;5;13:*.cgm=38;5;13:*.emf=38;5;13:*.axv=38;5;13:*.anx=38;5;13:*.ogv=38;5;13:*.ogx=38;5;13:*.aac=38;5;45:*.au=38;5;45:*.flac=38;5;45:*.mid=38;5;45:*.midi=38;5;45:*.mka=38;5;45:*.mp3=38;5;45:*.mpc=38;5;45:*.ogg=38;5;45:*.ra=38;5;45:*.wav=38;5;45:*.axa=38;5;45:*.oga=38;5;45:*.spx=38;5;45:*.xspf=38;5;45: PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/czanik/.local/bin:/home/czanik/bin LANG=en_US.UTF-8 MAIL=/var/mail/root LOGNAME=root USER=root HOME=/root SHELL=/bin/bash SUDO_COMMAND=/bin/ls / SUDO_USER=czanik SUDO_UID=1000 SUDO_GID=1000
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
[czanik@centos7sudo ~]$
Based on the feedback I’ve gotten from demos in my sudo talks, I cannot emphasize enough that this is just an introductory example. However, it should be enough to get you started experimenting with the Audit Plugin API.
If you would like to be notified about new posts and sudo news, sign up for the sudo blog announcement mailing list.