puffy

Not long ago, I had to setup an Ubuntu VM (I'm running them through bhyve by the way, and it's really sweet). But the day after, to my surprise, the lovely heartbeat email $hostname daily run output I've been used to was nowhere to be found. After a quick check, the Ubuntu cron "periodic" stuff doesn't output anything unless something go wrong.

I could easily add scripts into the /etc/cron.daily directory, but then I would have to handle the output "by hand" if they should or should not report something and the email subject would be the ugly cron line: Cron <root@$hostname> test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )

Ultimately I realized that my dream was something like OpenBSD daily(8) that I am used to (they say old habits die hard). Since they are simple POSIX shell scripts, I've hacked them a bit to run on Ubuntu.

/etc/daily

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#
#	$OpenBSD: daily,v 1.88 2016/04/29 13:05:33 schwarze Exp $
#	From: @(#)daily	8.2 (Berkeley) 1/25/94
#
# For local additions, create the file /etc/daily.local.
# To get section headers, use the function next_part in daily.local.
#
umask 022

PARTOUT=/var/log/daily.part
MAINOUT=/var/log/daily.out
install -o 0 -g 0 -m 600    /dev/null $PARTOUT
install -o 0 -g 0 -m 600 -b /dev/null $MAINOUT

start_part() {
	TITLE=$1
	exec > $PARTOUT 2>&1
}

end_part() {
	exec >> $MAINOUT 2>&1
	test -s $PARTOUT || return
	echo ""
	echo "$TITLE"
	cat $PARTOUT
}

next_part() {
	end_part
	start_part "$1"
}

run_script() {
	f=/etc/$1
	test -e $f || return
	if [ `stat --format '%A%u' $f | cut -b1,6,9,11-` != '---0' ]; then
		echo "$f has insecure permissions, skipping:"
		ls -l $f
		return
	fi
	. $f
}

start_part "Running daily.local:"
run_script "daily.local"

# NOTE: Removed OpenBSD specific daily stuff.

next_part "Services that should be running but aren't:"
if systemctl --quiet is-failed '*'; then
	systemctl --failed
fi

next_part "Checking subsystem status:"
if [ "X$VERBOSESTATUS" != X0 ]; then
	echo ""
	echo "disks:"
	df -ikl
	if [ -x /sbin/dump ]; then
		echo ""
		/sbin/dump W
	fi;
fi

next_part "network:"
if [ "X$VERBOSESTATUS" != X0 ]; then
	netstat -ivn
fi

end_part
[ -s $MAINOUT ] && {
	uname -a
	echo
	uptime
	cat $MAINOUT
} 2>&1 | mail -s "`hostname` daily output" root

# NOTE: removed OpenBSD security(8) stuff.
I had to remove some OpenBSD specific daily stuff like security(8) and adapt a bit the rest like the stat(1) options at line 36.

/etc/weekly and /etc/monthly were easier:

/etc/weekly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#
#	$OpenBSD: weekly,v 1.27 2015/08/14 03:02:07 rzalamena Exp $
#
# For local additions, create the file /etc/weekly.local.
# To get section headers, use the function next_part in weekly.local.
#
umask 022

PARTOUT=/var/log/weekly.part
MAINOUT=/var/log/weekly.out
install -o 0 -g 0 -m 600    /dev/null $PARTOUT
install -o 0 -g 0 -m 600 -b /dev/null $MAINOUT

start_part() {
	TITLE=$1
	exec > $PARTOUT 2>&1
}

end_part() {
	exec >> $MAINOUT 2>&1
	test -s $PARTOUT || return
	echo ""
	echo "$TITLE"
	cat $PARTOUT
}

next_part() {
	end_part
	start_part "$1"
}

run_script() {
	f=/etc/$1
	test -e $f || return
	if [ `stat --format '%A%u' $f | cut -b1,6,9,11-` != '---0' ]; then
		echo "$f has insecure permissions, skipping:"
		ls -l $f
		return
	fi
	. $f
}

start_part "Running weekly.local:"
run_script "weekly.local"

# NOTE: Removed OpenBSD specific weekly stuff.

end_part
rm -f $PARTOUT

[ -s $MAINOUT ] && mail -s "`hostname` weekly output" root < $MAINOUT

/etc/monthly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#
#	$OpenBSD: monthly,v 1.13 2011/01/19 06:18:05 david Exp $
#
# For local additions, create the file /etc/monthly.local.
# To get section headers, use the function next_part in monthly.local.
#
umask 022

PARTOUT=/var/log/monthly.part
MAINOUT=/var/log/monthly.out
install -o 0 -g 0 -m 600    /dev/null $PARTOUT
install -o 0 -g 0 -m 600 -b /dev/null $MAINOUT

start_part() {
	TITLE=$1
	exec > $PARTOUT 2>&1
}

end_part() {
	exec >> $MAINOUT 2>&1
	test -s $PARTOUT || return
	echo ""
	echo "$TITLE"
	cat $PARTOUT
}

next_part() {
	end_part
	start_part "$1"
}

run_script() {
	f=/etc/$1
	test -e $f || return
	if [ `stat --format '%A%u' $f | cut -b1,6,9,11-` != '---0' ]; then
		echo "$f has insecure permissions, skipping:"
		ls -l $f
		return
	fi
	. $f
}

start_part "Running monthly.local:"
run_script "monthly.local"

end_part
rm -f $PARTOUT

[ -s $MAINOUT ] && mail -s "`hostname` monthly output" root < $MAINOUT

Then, the system's /etc/crontab has to be adapted to actually run them. I used the same timing as OpenBSD (starting at line 17):

/etc/crontab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user	command
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

# OpenBSD-like daily stuff.
30 1	* * *	root	/bin/sh /etc/daily
30 3	* * 6	root	/bin/sh /etc/weekly
30 5	1 * *	root	/bin/sh /etc/monthly

Finally, here is my /etc/daily.local. It checks for packages and security updates along with Let's Encrypt SSL certificates renewal:

/etc/daily.local

1
2
3
4
5
6
7
8
9
10
11
12
13
# hacked from /etc/update-motd.d/90-updates-available to only show something
# when there are update(s) available.
next_part "Ubuntu packages:"
stamp="/var/lib/update-notifier/updates-available"
if [ -r "$stamp" ]; then
	awk 'int($1) {print}' "$stamp"
fi

next_part "renew Let's Encrypt certificates:"
letsencrypt --text --noninteractive --agree-tos renew
# XXX: no way of knowing with this client version if we actually have renewed a
# certificat, so reload nginx anyway.
systemctl reload nginx

And that's it! Let me know if this post was useful to you and if I missed anything of course. Happy new year by the way :)