Web gallery solution

A while back, I was inspired by Thealaskalinuxuser’s article about how he set up his photo sharing server at home. I experimented briefly with piwigo back when I read that, but ended up stopping that project. When I returned to this topic, I realized I just didn’t want to deal with php. I applaud his stamina and commitment, but I want something with fewer dependencies. Here is my solution.

So, here is my internal documentation that is my alternative to Google Photos.

Gallery solution for internal network

Overview

As part of my goals to run self-hosted services for myself and my family, I intend to maintain a web gallery of photos and videos.
This solution consists of several parts.

  • Tools that create symlink forests to the original image files, in a specific location
  • Static site generator sigal
  • Customized theme for sigal
  • Customized sigal config for that theme
  • Gallery id table
  • SELinux rules
  • CGI scripts for Apache httpd

Architecture

The solution runs on server1, the main file server for the internal network.

Tools that create symlink forests for the gallery source directories

Generate.sh is what I used for the proof of concept. This needs to be rewritten in python, and to handle files without exif data.
My python implementation will show up on this blog at a later date.

Static site generator

The sigal site generator is installed with pip3, under local service account `sigal` on server1. A special script, /usr/local/bin/sigal.bin will generate the static pages for a site. To use this script, pass a parameter of the source directory where the sigal.conf.py exists.

/usr/local/bin/sigal.bin /mnt/public/www/example/images/.gallery

Sigal depends on ffmpeg for video thumbnailing and conversions. On CentOS 8, ffmpeg is in repository powertools.
File sigal.bin

#!/bin/sh
# File: /usr/local/bin/sigal.bin
# Author: bgstack15@gmail.com
# Startdate: 2021-01-24
# Title: Run sigal static site generator
# Purpose: run sigal static site generator for provided path
# History:
# Usage:
#    called by regen.cgi, or by hand
# Dependencies:
#    60_regen_gallery_sudo
#    gallery.te
#    service account "sigal" with `pip3 install --user sigal`
# Reverse dependencies:
#    httpd conf 
sigal_home=~sigal

test -z "${GALLERY_ID}" && export GALLERY_ID="${1}"
test -z "${GALLERY_ID}" && { echo "Pass to this script the gallery_id or directory path to a sigal.conf.py. Aborted." 1>&2 ; exit 1 ; }
if test -d "${GALLERY_ID}" ;
then
   cd "${GALLERY_ID}"
else
   if test -e /etc/gallery-cgi.conf ;
   then
      . /etc/gallery-cgi.conf
   else
      # no gallery_id listing exists.
      echo "No gallery_id table exists. Aborted." 1>&2 ; exit 1
   fi
fi
if test "${GALLERY_ID}" = "init" ;
then
   sudo su sigal -s /bin/bash -c "${sigal_home}/.local/bin/sigal init"
else
   eval cd \"\${"${GALLERY_ID}"}\"
   sudo su sigal -s /bin/bash -c "${sigal_home}/.local/bin/sigal build"
fi

Customized theme for sigal

Use theme bgstack15-gallery-theme, in any location. You just need to put its full path as the value of the theme in a sigal.conf.py for a gallery.
This is a fork of the default, included colorbox theme. The full theme is available in this project directory, as well as a diff of it to the original as of sigal version 2.1.1.
The main changes are adding extra metadata handlers and link logic for the custom links related to editing metadata.

Customized sigal config for that theme

When you run sigal init in a directory, it generates a default config you can modify. The example theme, and this whole solution, depends on adding a number of settings. A summary of the specific options is here, but the full example file is in this project directory.

source = '/var/www/gallery/.my2018'
destination = '/var/www/gallery/my2018'
use_orig = True
edit_cgi_script = "/cgi-bin/gallery/edit.cgi" # web path to edit.cgi
edit_enabled = False
edit_password = "makeupapassword"
edit_string = '[edit]' # text of link to edit metadata
toggle_enable_string = "Enable editing"
toggle_disable_string = "Disable editing"
toggle_link = "/cgi-bin/gallery/toggle-editing.cgi" # web path to toggle-editing.cgi
toggle_editing = [
        (False, 'Enable editing'),
        (True, 'Disable editing')
    ]
gallery_id = "example_images"
# A list of links (tuples (title, URL))
# links = [('Example link', 'http://example.org'),
#          ('Another link', 'http://example.org')]
links = [
    ('Regenerate pages', '/cgi-bin/gallery/regen.cgi?id=' + gallery_id)
        ]

The `gallery_id` is very important, because the cgi scripts and theme rely on it.

Gallery id table

File /etc/gallery-cgi.conf contains a list of gallery_id translations to directories with sigal.conf.py rules.

example_images=/mnt/public/www/example/images/.gallery

SELinux rules

The reference system, server1, runs SELinux. A custom selinux module is needed to allow all the operations that are a part of this gallery solution, which include the following.

File gallery.te can be installed as an enabled selinux module.

sudo checkmodule -M -m -o gallery.mod gallery.te && sudo semodule_package -o gallery.pp -m gallery.mod && sudo semodule -i gallery.pp

File gallery.te:

# Last modified 2021-01-30
module gallery 1.0;

require {
	type faillog_t;
	type security_t;
	type httpd_config_t;
	type init_t;
	type sssd_t;
	type mnt_t;
	type lastlog_t;
	type systemd_logind_sessions_t;
	type initrc_var_run_t;
	type tmpfs_t;
	type gconf_home_t;
	type chkpwd_t;
	type systemd_logind_t;
	type unconfined_t;
	type shadow_t;
	type httpd_sys_script_t;
	type sssd_selinux_manager_t;
	type sssd_conf_t;
	type var_t;
	type httpd_t;
	class capability { audit_write dac_read_search net_admin setgid setuid sys_resource };
	class process { noatsecure rlimitinh setrlimit siginh };
	class netlink_audit_socket { create nlmsg_relay read write };
	class netlink_selinux_socket { bind create };
	class passwd rootok;
	class dir { add_name read remove_name search write };
	class file { create execute execute_no_trans setattr getattr link lock map open read unlink write ioctl };
	class dbus send_msg;
	class fifo_file write;
	class security compute_av;
	class lnk_file read;
	class filesystem getattr;
	class process setfscreate;
}

#============= httpd_sys_script_t ==============
allow httpd_sys_script_t faillog_t:file { open read };
allow httpd_sys_script_t var_t:file { create ioctl setattr unlink write };
allow httpd_sys_script_t var_t:dir { read add_name remove_name write };

#!!!! This avc can be allowed using the boolean 'domain_can_mmap_files'
allow httpd_sys_script_t gconf_home_t:file map;
allow httpd_sys_script_t gconf_home_t:file { execute execute_no_trans };
allow httpd_sys_script_t httpd_config_t:dir search;
allow httpd_sys_script_t initrc_var_run_t:file { lock open read };
allow httpd_sys_script_t lastlog_t:file { open read write };
allow httpd_sys_script_t mnt_t:lnk_file read;
allow httpd_sys_script_t security_t:dir read;
allow httpd_sys_script_t security_t:file { getattr open read write };
allow httpd_sys_script_t security_t:security compute_av;
allow httpd_sys_script_t self:capability { audit_write dac_read_search net_admin setgid setuid sys_resource };
allow httpd_sys_script_t self:netlink_audit_socket { create nlmsg_relay read write };
allow httpd_sys_script_t self:netlink_selinux_socket { bind create };
allow httpd_sys_script_t self:passwd rootok;
allow httpd_sys_script_t self:process setrlimit;
allow httpd_sys_script_t shadow_t:file { getattr open read };
allow httpd_sys_script_t sssd_conf_t:dir search;
allow httpd_sys_script_t sssd_conf_t:file { getattr open read };
allow httpd_sys_script_t systemd_logind_sessions_t:fifo_file write;
allow httpd_sys_script_t systemd_logind_t:dbus send_msg;
allow httpd_sys_script_t tmpfs_t:dir { add_name remove_name write };

#!!!! This avc can be allowed using the boolean 'domain_can_mmap_files'
allow httpd_sys_script_t tmpfs_t:file map;
allow httpd_sys_script_t tmpfs_t:file { create getattr link open read unlink write };
allow httpd_sys_script_t tmpfs_t:filesystem getattr;
allow httpd_sys_script_t self:process setfscreate;

#============= init_t ==============
allow init_t chkpwd_t:process siginh;
allow init_t unconfined_t:process siginh;

#============= sssd_t ==============
allow sssd_t sssd_selinux_manager_t:process { noatsecure rlimitinh siginh };

#============= systemd_logind_t ==============
allow systemd_logind_t httpd_sys_script_t:dbus send_msg;
#============= httpd_t ==============
allow httpd_t var_t:file { getattr map open read };

Sudo rules

For apache httpd to be able to run the sigal.bin, set up sudoers rules. File 60_regen_gallery_sudo adds the permission necessary.

# file: /etc/sudoers.d/60_regen_gallery_sudo
# Reference: server4:/etc/sudoers.d/60_starbound_sudo
apache ALL=(root)   NOPASSWD: /usr/local/bin/sigal.bin *

CGI scripts for Apache httpd

The main focus of the gallery project is the ability to edit metadata from the web view. While sigal is great for developers, some users might only care about editing metadata from where they are actually viewing the media.

  • edit.cgi is called from the custom theme’s “edit” links, and includes the form for making changes to media metadata.
  • apply.cgi actually makes the changes, and is called from the edit.cgi form.
  • regen.cgi invokes sigal.bin which re-runs sigal.
  • toggle-editing.cgi enables or disables editing. Enabling requires a password.

These can be placed anywhere you have enabled CGI for httpd, but the canonical location is /var/www/cgi-bin/gallery/.

Operations

I anticipate that more work is needed on an ongoing basis. Here are some processes that can be used.

Make a new gallery

To establish a new gallery, change directory to the source directory for the gallery and run command

sigal.bin init

Which generates the basic sigal.conf.py. Add the pertinent variables, described in section “Customized sigal config for that theme” above.

Run sigal from command line

While the web links for “regen.cgi” are great for when you are viewing the web, you can also run the sigal.bin from the cli. You need to include a path to the directory that holds a sigal.conf.py, or else a gallery_id from /etc/gallery-cgi.conf.

sigal.bin example_images

Make metadata changes directly on filesystem

You can of course, as designed by the author of sigal, go edit any ${IMAGENAME%%.jpg}.md file with the relevant fields. See references 1 and 2 for the available fields.
File index.md will be the metadata for the directory itself.

History

In 2020, I installed piwigo on a dev system. I didn’t want to deal with php, so I dropped it. In January 2021, I started listing various options for self-hosted galleries. Read heading [Related Files] for those.
Criteria I assembled includes

  • Metadata: description, date, comments

Related Files

These files are important to this gallery project. Check them all out at my gitlab space.

  • /usr/local/bin/sigal.bin
  • /var/www/cgi-bin/apply.cgi
  • /var/www/cgi-bin/edit.cgi
  • /var/www/cgi-bin/regen.cgi
  • /var/www/cgi-bin/toggle-editing.cgi
  • gallery.te
  • /etc/sudoers.d/60_regen_gallery_sudo
  • /etc/gallery-cgi.conf
  • sigal.conf.py
  • bgstack15-gallery-theme/

Alternatives

Ones I considered without trying

Ones I listed as tolerable, but not focused on what I need.

References

Weblinks

  1. http://sigal.saimon.org/en/latest/album_information.html
  2. http://sigal.saimon.org/en/latest/image_information.html
  3. Home photo server, part 2: Apache, phpAlbum, and Piwigo | thealaskalinuxuser Thealaskalinuxuser’s guide to a home photo server with piwigo

Plex Activity Logging and Alerting Project

Overview

Plex media server has the ability to send webhooks when used with a Plex Pass (premium) account. This project documents how to set up a webhook to log and alert on media activity.
The design goals include logging all media playing activity (play, stop, pause, resume) on my Plex server, and sending email notifications.
Technologies used: apache, shell, selinux, jq, mailx

Architecture

Plex can be configured to point to a webhook, e.g., http://plex.example.com/cgi-bin/plex-log.cgi. The webhook file is documented below. The webhook is a script that acts upon the data passed from Plex. The actions include copying important values to different log files, and invoking a separate script that sends an email to the admin.

Involved files

Multiple files are a part of this project.

  • /var/server1/shares/public/www/cgi-bin/plex-log.cgi
  • /var/server1/shares/public/Support/Systems/server1/var/log/plex-webhook/activity.log
  • /var/server1/shares/public/Support/Systems/server1/var/log/plex-webhook/short.log
  • /usr/local/bin/send-plex-alert.sh
  • /usr/src/selinux/plexlog.te

Webhook script

#!/bin/sh
# File: server1:/var/server1/shares/public/www/cgi-bin/plex-log.cgi
# License: CC-BY-SA 4.0
# Author: bgstack15
# Startdate: 2020-08-06 07:55
# Title: Logging Webhook for Plex
# Project: Plex Activity Logging and Alerting Project
# History:
# References:
#    send from bgscripts
#    https://bgstack15.wordpress.com/2017/08/05/send-authenticated-gmail-from-cli-with-mailx/
#    Not sure which mail I have https://unix.stackexchange.com/questions/15405/how-do-i-send-html-email-using-linux-mail-command/15463#15463
# Related files:
#    /usr/local/bin/send-plex-alert.sh
# Improve:
# Dependencies:
#    /usr/local/bin/send-plex-alert.sh
#    Webhook functionality of Plex with a Plex Pass account
#    apache cgi capability
# Documentation:
#    /mnt/public/Support/Programs/Plex/webhooks/palap-readme.md
LOGFILE=/var/server1/shares/public/Support/Systems/server1/var/log/plex-webhook/activity.log
LOGFILESHORT=/var/server1/shares/public/Support/Systems/server1/var/log/plex-webhook/short.log
VERBOSE=0 # set to anything to display extra things
ALERT_ON_EVENTS="play:stop:pause:resume" # options include play stop resume pause, colon separated
NOW="$( date -u "+%FT%TZ" )"
TMPFILE1="$( mktemp )"
TMPFILE2="$( mktemp )"
TMPFILE3="$( mktemp )"
printf "%s\n\n" "Content-type: text/html"
echo "<html><head><title>Plex webhook</title>"
echo "</head><body>"
echo "<pre>"
cat > "${TMPFILE1}"
boundary="$( echo "${CONTENT_TYPE}" | awk '{print $2}' | awk -F'=' '{print $2}' )"
{
	test "${VERBOSE}" -gt 0 && {
	   printf "%s\n" "" >> "${LOGFILE}"
      echo "REQUEST-START: ${NOW}"
   	env
   	set
   	echo "BEGIN RAW"
   }
	sed -r -e "s/-*${boundary}-*/\n/g;" "${TMPFILE1}" | tee "${TMPFILE2}" | \
      {
         test "${VERBOSE}" -gt 0 && { cat ; } || { cat >/dev/null ; }
      }
	test "${VERBOSE}" -gt 0 && {
      echo "END RAW"
      echo "BEGIN JQ"
   }
   json="$( grep -aE "^.{0,4}\"event\"" "${TMPFILE2}" | jq -c ". += {timestamp: \"${NOW}\", useragent: \"${HTTP_USER_AGENT}\" }" 2>&1 )"
   # full json for log
   echo "${json}"
   # send truncated json to mail account for alerts
   echo "${json}" | jq '{timestamp: .timestamp, event: .event, user: .Account.title, player: .Player.title, ipAddress: .Player.publicAddress, file: {type: .Metadata.type, title: .Metadata.title, parentTitle: .Metadata.parentTitle, grandparentTitle: .Metadata.grandparentTitle } }' > "${TMPFILE3}" 
   # log to short log and send email only if it has contents
   printf "%s" "${json}" | grep -qE '.' && {
      cat "${TMPFILE3}" >> "${LOGFILESHORT}"
      # only alert if it matches. This syntax relies on the event syntax from Plex to be "media.play" or "media.stop" et al.
      alert_regex="(media\.)$( echo "${ALERT_ON_EVENTS}" | sed -r -e 's/^://g;' -e 's/:$//g;' -e 's/:+/|/g;' )"
      if grep -qE "${alert_regex}" "${TMPFILE3}" ;
      then
         # magic to get fixed-width font in gmail by using html pre tags
         { echo "<html><body><pre>" ; cat "${TMPFILE3}" ; echo "</pre></html></body>" ; } | mailsubject="Media activity
Content-Type: text/html" /usr/local/bin/send-plex-alert.sh STDIN
      else
         test "${VERBOSE}" -gt 0 && {
            echo "SKIPPING EMAIL because event does not match ${ALERT_ON_EVENTS}"
         }
      fi
   }
   test "${VERBOSE}" -gt 0 && {
      echo "END JQ"
      echo "REQUEST-STOP: ${NOW}"
   }
} | tee -a "${LOGFILE}"
echo "</pre>"
echo "</body></html>"

# clean up
rm -f "${TMPFILE1}" "${TMPFILE2}" "${TMPFILE3}"

The above webhook script contains its own comments. The VERBOSE attribute can be set to a non-zero amount (usually 1) to log much more information useful for debugging. Attribute ALERT_ON_EVENTS is a colon-separated list of event types passed from Plex that should trigger an email to the admin. This syntax depends on Plex’s syntax not changing.

The two log files are configured in the main webhook script. The LOGFILE contains the verbose logging (if enabled) and the full json object from Plex. The LOGFILESHORT contains the trimmed contents that will be used for the email.

Email script

A separate script holds the logic to send the email, file /usr/local/bin/send-plex-alert.sh.

#!/bin/sh
# File: send-plex-alert.sh
# Project: Plex Activity Logging and Alerting Project
# startdate: 2020-08-07 17:21
# Documentation:
#    this one worked, but took a while, probably due to ISP smtp relay delay. echo "echo juliet oscar 10" | mailx -S 'from="bgstack15@ipa.example.com"' -S "Subject line" bgstack15@gmail.com ; echo $?
#    worked immediately: echo "hotel whiskey zulu 14" | mailx -v -S "Another thread here" -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp=smtp://smtp.gmail.com:587 -S from="B Stack <bgstack15@gmail.com>" -S smtp-auth-user="bgstack15@gmail.com" -S smtp-auth-password='FIXME' -S nss-config-dir=/etc/pki/nssdb/ bgstack15@gmail.com
# Improve:
#    learn how to use a custom MAILRC env var which holds the "set smtp-user-password=" contents
# Dependencies:
#    mailx (mailx-12.5-19.el7.x86_64 @base)
#    Google account security set to allow "insecure apps" or similar https://support.google.com/accounts/answer/6010255?authuser=2&p=lsa_blocked&hl=en&authuser=2&visit_id=637324418441299012-2559162990&rd=1

test -z "${mailmessage}" && export mailmessage="${1}"
test "${mailmessage}" = "STDIN" && export mailmessage="$( cat 2>/dev/null )"
test -z "${mailuser}" && export mailuser="bgstack15@gmail.com"
test -z "${mailpassword}" && export mailpassword='SUPERSAFESTRING;'
test -z "${mailfrom}" && export mailfrom="Plex activity <bgstack15@gmail.com>"
test -z "${mailsubject}" && export mailsubject="Media activity"
test -z "${mailto}" && export mailto=bgstack15@gmail.com
printf "%s\n" "${mailmessage}" | mailx -s "${mailsubject}" \
   -S smtp-use-starttls \
   -S ssl-verify=ignore \
   -S smtp=smtp://smtp.gmail.com:587 \
   -S smtp-auth=login \
   -S from="${mailfrom}" \
   -S smtp-auth-user="${mailuser}" \
   -S smtp-auth-password="${mailpassword}" \
   -S nss-config-dir=/etc/pki/nssdb \
   "${mailto}"

SELinux policy

To use the webhook script and mail script with SELinux enforcing, you need to add a custom policy. File /usr/src/selinux/plexlog.te contains the uncompiled policy.

# File: /usr/src/selinux/plexlog.te
# Startdate: 2020-08-07 20:45
# Title: SELinux policy to allow webhook to send email
# Project: Plex Activity Logging and Alerting Project
module plexlog 1.0;

require {
	type httpd_sys_script_t;
	type init_t;
	type httpd_t;
	type smtp_port_t;
	type var_t;
	class process { noatsecure rlimitinh siginh };
	class unix_stream_socket { read write };
	class capability net_admin;
	class file { append execute getattr open read };
	class tcp_socket name_connect;
}

#============= httpd_sys_script_t ==============

#!!!! WARNING: 'var_t' is a base type.
allow httpd_sys_script_t var_t:file { append execute getattr open read };

#!!!! This avc can be allowed using one of the these booleans:
#     httpd_can_network_connect, nis_enabled
allow httpd_sys_script_t smtp_port_t:tcp_socket name_connect;

#============= httpd_t ==============
allow httpd_t httpd_sys_script_t:process { noatsecure rlimitinh siginh };
allow httpd_t self:capability net_admin;

To use this policy, you need to compile and install it.

sudo checkmodule -M -m -o plexlog.mod plexlog.te && sudo semodule_package -o plexlog.pp -m plexlog.mod && sudo semodule -i plexlog.pp

References

Weblinks

  1. Running Starbound server on CentOS 7
  2. Technical Notes: How to setup/configure/use mailx for Office365 account?

Run init script as SELinux type other than initrc_t

To run a custom init script as SELinux context other than initrc_t, you can use an SELinux policy that adds a new type for you to use.

# Filename: general-local.te
# License: CC-BY-SA 4.0
# Author: bgstack15
# Startdate: 2019-09-19 16:45
# Title: SELinux Policy for Custom Process Types from Init Scripts
# Purpose: SELinux policy to allow an init script to run a process as a selinux type other than initrc_t
# History:
# Usage:
#    When installed, you can run the following command to have the daemon process transition to type unconfined_t:
#    chcon -t 'local_initrc_exec_t' /etc/init.d/myscript
# Reference:
#    liberal use of tail -n45000 /var/log/audit/audit.log | audit2allow
#    https://selinuxproject.org/page/ObjectClassesPerms#filesystem
#    http://www.cse.psu.edu/~trj1/cse543-f07/slides/03-PolicyConcepts.pdf
#    http://www.billauer.co.il/selinux-policy-module-howto.html
#    https://fedoraproject.org/wiki/PackagingDrafts/SELinux#Creating_new_types
#    https://wiki.centos.org/HowTos/SELinux
#    https://bgstack15.wordpress.com/2018/02/13/logrotate-audit-log-selinux-cron-and-ansible/
# Improve:
# Documentation:
#    Change an init script to context local_initrc_exec_t and then the process will transition to unconfined_t which of course is insecure, but it satisfies the scan that is looking for daemons running as initrc_t.
module general-local 1.0;

require {
        type fs_t;
        type initrc_exec_t;
        type init_t;
        type unconfined_t;
        class file { append create entrypoint execmod execute execute_no_trans getattr ioctl link lock mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink write };
        class filesystem associate;
        class process { unconfined transition };
        class service { start status };
}

type local_initrc_exec_t;
type_transition init_t local_initrc_exec_t:process unconfined_t ;

#============= init_t ==============
allow init_t local_initrc_exec_t:file *;
allow init_t unconfined_t:process transition;
#============= local_initrc_exec_t ==============
allow local_initrc_exec_t fs_t:filesystem associate;
#============= unconfined_t ==============
allow unconfined_t local_initrc_exec_t:file *;
allow unconfined_t local_initrc_exec_t:service { start status };

To compile and install this module, you can run the following oneliner.

checkmodule -M -m -o general_local.mod general_local.te && semodule_package -m general_local.mod -o general_local.pp && semodule -v -i general_local.pp

Should you run daemons as unconfined_t? Of course not. But it’s different than running it as initrc_t.

References

Weblinks

  1. ObjectClassesPerms – SELinux Wiki
  2. SELinux Policy Concepts and Overview: Security Policy Development Primer for Security Enhanced Linux
  3. Writing a targeted policy module for SELinux (howto tutorial slides)
  4. PackagingDrafts/SELinux – Fedora Project Wiki#Creating_new_types
  5. HowTos/SELinux – CentOS Wiki
  6. Logrotate, audit.log, selinux, cron, and ansible | Knowledge Base

 

Logrotate, audit.log, selinux, cron, and ansible

The story

The disk space for /var/log/audit/audit.log tends to get filled up. The audit daemon has an ability to rotate its own logs. See the man page for auditd.conf.

max_log_file             =  100
max_log_file_action      =  rotate

That’s swell and all, until you realize that auditd cannot compress its rotated logs. On a small /var/log/audit mount point, you’ll fill it up with uncompressed logs.

/dev/mapper/os-var_log_audit          2490M 2136M      355M      86% /var/log/audit

So on a RHEL7 system with logrotate, you can adjust logrotate to handle the audit.log file. Now, logrotate is a finicky application. It has caused me many hours of grief in the past.
You would want to set auditd.conf a certain way:

max_log_file             =  0
max_log_file_action      =  ignore

And set /etc/logrotate.d/audit:

/var/log/audit/*.log {
        weekly
        missingok
        compress
        #copytruncate
        rotate 30
        minsize 100k
        maxsize 200M
        postrotate
                touch /var/log/audit/audit.log ||:
                chmod 0600 /var/log/audit/audit.log ||:
                service auditd restart
        endscript
}

And ensure you’ve got a /etc/cron.weekly/logrotate:

#!/bin/sh

/usr/sbin/logrotate /etc/logrotate.conf
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
    /usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit 0

After a few days, I learned that my logs were getting filled up so fast, the weekly rotation wasn’t good enough. So I had to place it in my cron.hourly.
And then I learned that it wasn’t running every hour. I spent a few days investigating, and eventually learned that some systems use a specific status file for logrotate. I remember in the past logrotate needs an execution with a -f flag to force the rotation the first time and add a new file to the status file. So if a new file was never force-rotated, it won’t be added to the status file.
My manual logrotate -f command was indeed adding my audit.log log file to the status file, but to the wrong one!
Some of my systems use -s /var/lib/logrotate/logrotate.status but the default is /var/lib/logrotate.status.
So I had to reflect that in my ansible playbook. Actually, I had to write some logic to find the one used by the cronjob and then use that status file.

So I got the correct logrotate status file set up in the ansible playbook. I spent the next week figuring out that logrotate simply couldn’t rotate the file when called from cron. I piped the utility to tee, and also included the -v flag on logrotate. I saw a permission denied.
With the permission issue, I had no choices left by selinux. I had to use the audit.log file to determine that the audit.log file is not readable by logrotate when called by cron.
I finally set captured all the actions performed by logrotate by setting the selinux process context to be permissive:

semanage permissive -a logrotate_t
I let it run, and then had to collect all the actions it performed, and saw what had happened.
{ grep logrotate /var/log/audit/audit.log ; zgrep logrotate /var/log/audit/audit.log.1.gz ; } | audit2why

So I used audit2allow to convert it to an selinux policy.

{ grep logrotate /var/log/audit/audit.log ; zgrep logrotate /var/log/audit/audit.log.1.gz ; } | audit2allow -M logrotate-audit

And then after some searching online, I learned how I can keep the text definition file, and compile the policy from it when I need to:

grep logrotate /var/log/audit/audit.log | audit2allow -m logrotate-audit # saves to logrotate-audit.te
checkmodule -M -m -o logrotate-audit.mod logrotate-audit.te # intermediate step
semodule_package -o logrotate-audit.pp -m logrotate-audit.mod # compiled policy
semodule -i logrotate-audit.pp

The text definition of logrotate-audit policy:

#semodule -i logrotate-audit.pp

module logrotate-audit 1.0;

require {
        type auditd_etc_t;
        type logrotate_t;
        type auditd_log_t;
        class file { create getattr ioctl open read rename setattr unlink write };
        class dir { add_name read remove_name write };
}

#============= logrotate_t ==============
allow logrotate_t auditd_etc_t:file getattr;
allow logrotate_t auditd_log_t:dir { read write add_name remove_name };
allow logrotate_t auditd_log_t:file { create ioctl open read rename getattr setattr unlink write };

Now, I wrote a master ansible playbook that performs this whole operation, from loading the .te file and compiling it and installing it, to setting logrotate to watch the audit file, and telling auditd to ignore rotating it.
Note: It is outside the scope of this task to ensure that the selinux tools are in place on each server. My environment already ensures package libselinux-python is present on each system, which should bring in all the dependencies of this ansible playbook.

---
# File: /etc/ansible/books/fix_var-log-audit.yml
# Author: bgstack15
# Startdate: 2018-01-24
# Title: Playbook that Fixes the /var/log/audit Space Issue
# Purpose: Logical Disk Free Space is too low
# History:
# Usage:
#    ansible-playbook -i /etc/ansible/inv/hosts /etc/ansible/configuration/fix_var-log-audit.yml -l hostwithproblem201
#    Use the -l host1,host2 parameter.
# Reference:
#    roles/general_conf/tasks/04_selinux.yml
#    roles/general_conf/tasks/05_auditd.yml
# Improve:
# Documentation:
#    The intention with auditd is to minimize the disk usage of the logs

- hosts: all
  remote_user: ansible_user
  become: yes

  vars:
    auditd_conf: /etc/audit/auditd.conf
    auditd_log_cleanup_regex: '.*audit\.log\.[0-9]+'
    auditd_log_dir: /var/log/audit
    auditd_logrotate_conf: /etc/logrotate.d/audit

  tasks:

# To make it possible to just drop in files to the files directory and have this module read them automatically, use these two.
#  - name: learn full list of semodules available to install, modular list version
#    shell: warn=no find /etc/ansible/roles/general_conf/files/selinux/ -regex '.*.te' -printf '%f\n' | sed -r -e 's/\.te$//;'
#    register: semodules_list
#    changed_when: false
#    delegate_to: localhost
#    ignore_errors: yes
	
#  - name: learn semodule versions to install, modular list version
#	shell: warn=no grep -E '^\s*module\s+{{ item }}\s+[0-9\.]+;\s*$' /etc/ansible/roles/general_conf/files/selinux/{{ item }}.te | awk '{print $3*1000;}'
#    register: selinux_pol_versions_target
#    changed_when: false
#    delegate_to: localhost
#    with_items:
#    - "{{ semodules_list.stdout_lines }}"

  - name: learn semodule versions to install, static version
    shell: warn=no grep -E '^\s*module\s+{{ item }}\s+[0-9\.]+;\s*$' /etc/ansible/templates/{{ item }}.te | awk '{print $3*1000;}'
    register: selinux_pol_versions_target
    changed_when: false
    delegate_to: localhost
    with_items:
    - logrotate-audit

  #- debug:
  #    msg: "{{ item.item }} should be {{ item.stdout }}"
  #  with_items:
  #  - "{{ selinux_pol_versions_target.results }}"

  - name: learn current semodule versions
    shell: warn=no semodule --list | awk '$1=="{{ item.item }}" {print $2*1000} END {print "0";}' | head -n1
    register: selinux_pol_versions_current
    changed_when: false
    with_items:
    - "{{ selinux_pol_versions_target.results }}"

  - debug:
      msg: "{{ item.item.item }} is currently {{ item.stdout }} and should be {{ item.item.stdout }}"
    with_items:
    - "{{ selinux_pol_versions_current.results }}"

  #- pause:
  #    prompt: "Does the above look good?........................"

  - name: download selinux modules that need to be installed
    copy:
      src: "/etc/ansible/templates/{{ item.item.item }}.te"
      dest: "/tmp/{{ item.item.item }}.te"
      mode: 0644
      owner: root
      group: root
      backup: no
      force: yes
    changed_when: false
    when:
    - "item.item.stdout > item.stdout"
    with_items:
    - "{{ selinux_pol_versions_current.results }}"

  - name: install selinux modules
    shell: chdir=/tmp warn=no /usr/bin/checkmodule -M -m -o "/tmp/{{ item.item.item }}.mod" "/tmp/{{ item.item.item }}.te" && /usr/bin/semodule_package -m "/tmp/{{ item.item.item }}.mod" -o "/tmp/{{ item.item.item }}.pp" && /usr/sbin/semodule -v -i "/tmp/{{ item.item.item }}.pp"
    when:
    - "item.item.stdout > item.stdout"
    with_items:
    - "{{ selinux_pol_versions_current.results }}"

  - name: clean any temporary selinux modules files
    file:
      path: "/tmp/{{ item[0].item.item }}.{{ item[1] }}"
      state: absent
    changed_when: false
    when:
    - "item[0].item.stdout > item[0].stdout"
    with_nested:
    - "{{ selinux_pol_versions_current.results }}"
    - [ 'te', 'pp', 'mod' ]

##### END SELINUX PORTION

  # modify auditd.conf which notifies the handler
  - name: auditd does not keep logs
    lineinfile:
      path: "{{ auditd_conf }}"
      regexp: "{{ item.r }}"
      backrefs: yes
      line: "{{ item.l }}"
      create: no
      state: present
      backup: yes
    #notify: auditd handler
    with_items:
    - { r: '^max_log_file_action.*$', l: 'max_log_file_action      =  ignore' }
    - { r: '^max_log_file\s.*$', l: 'max_log_file             =  0' }

  # tarball and cleanup any existing audit.log.1 files
  - name: list all old auditd logs which need to be compressed and cleaned up
    shell: warn=no find /var/log/audit -regex {{ auditd_log_cleanup_regex }}
    register: cleanup_list
    ignore_errors: yes
    changed_when: cleanup_list.stdout_lines | length > 0

  - name: get archive filename
    shell: warn=no echo "audit.log.{{ ansible_date_time.epoch }}.tgz"
    register: audit_log_tgz
    changed_when: audit_log_tgz.stdout_lines | length != 1

  - name: touch archive file
    file:
      path: "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}"
      state: touch
      owner: root
      group: root
      mode: 0600
    when: cleanup_list.stdout_lines | length > 0

  - name: archive and cleanup existing audit.log.1 files
    archive:
      dest: "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}"
      path: "{{ cleanup_list.stdout_lines }}"
      format: gz
      owner: root
      group: root
      remove: yes
    ignore_errors: yes
    when: cleanup_list.stdout_lines | length > 0

  - name: check for existence of new tarball
    stat:
      path: "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}"
    ignore_errors: yes
    register: audit_log_tarball

  - name: place audit log tarball in auditd_log_dir
    shell: warn=no /bin/mv "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}" "{{ auditd_log_dir }}/"
    ignore_errors: yes
    when:
    - audit_log_tarball.stat.exists is defined
    - audit_log_tarball.stat.exists

  - name: get current size of audit log
    stat:
      path: "{{ auditd_log_dir }}/audit.log"
    ignore_errors: yes
    register: audit_log_stat

  - name: apply logrotate script for audit
    copy:
      src: /etc/ansible/templates/etc-logrotate.d-audit
      dest: "{{ auditd_logrotate_conf }}"
      owner: root
      group: root
      mode: 0644
      backup: yes

  - name: learn the logrotate.status file to use, if any
    shell: warn=no grep -rE -- 'bin\/logrotate\>.*(-s|--state)(\s|=)[\/[A-Za-z0-9\.]+\>' /etc/cron.* 2>/dev/null | grep -oE '(-s|--state)(\s|=)[\/[A-Za-z0-9\.]+\>' | sort | uniq | head -n1
    ignore_errors: yes
    changed_when: false
    register: this_logrotate_flag

  - name: show which logrotate.status file to use, if any
    debug:
      msg: "The status file that will be used is {{ this_logrotate_flag.stdout }}"

  - name: run logrotate
    shell: warn=no /usr/sbin/logrotate {{ this_logrotate_flag.stdout }} -f "{{ auditd_logrotate_conf }}"
    register: run_logrotate
    when: ( cleanup_list.stdout_lines | length > 0 ) or ( audit_log_stat.stat.exists and audit_log_stat.stat.size > 190000000 )

  handlers:
...

Summary

So, logrotate can be configured to rotate the audit log. It just takes a few minutes to configure correctly, after about 2 weeks of research and testing.

References

Weblinks

  1. http://melikedev.com/2013/08/19/linux-selinux-semodule-compile-pp-module-from-te-file/
  2. https://linux.die.net/man/8/auditd.conf

Personal effort

Hours and hours of my original research
Years of administering RHEL servers with logrotate

Docker cannot write to mounted volume

So you’ve already investigated the permissions, and the selinux context. There are no errors in the audit logs.

And if you’re using a directory like /var/lib/docker/db, it will have context unconfined_u:object_r:container_var_lib_t:s0.

For mounting with -v /var/lib/docker/db/appname:/opt/application/ and it to be readable, you will need a new context.

semanage fcontext -a -t svirt_sandbox_file_t '/var/lib/docker/db(/.*)?'

Configure SELinux to allow Nagios publickey auth

Nagios is a tool for monitoring servers. In a security-minded environment, you need to make allowances for nagios. It operates over ssh using a public key, which SELinux doesn’t like.

One problem that can occur is that the ~nagios/.ssh/authorized_keys file will not have the right selinux context. Fix that with

semanage fcontext -a -t "ssh_home_t" "/var/spool/nagios(/.*)?"
restorecon -RvF /var/spool/nagios

This will make a new rule in selinux for that directory to have a regular ssh-homedir context, so public keys will work properly. If nagios cannot connect passwordlessly, it will throw fits.