Sudo
GitHub Blog Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

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.

Before you begin

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.

Configuring sudo

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.

Testing

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 ~]$ 

What is next?

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.