Secure linux configuration - according to UK NCSC anyway

I am considering my obligations to get UK ‘Cyber Essentials’ that includes secure configuration of all servers and workstations. That means, in broad terms, to not install software or services that is not needed (to do your job), change any default passwords, secure known weaknesses etc etc. It’s good to NOT do stupid things, yes?

The UK National Cyber Security Centre (NCSC) have released a script to secure Ubuntu after initial installation. I wondered what folks here think of it?

The article refers to a Github page that hosts a bash script, which you can get directly to here:

I have posted this to Tom Lawrence forums also, so I hope I do not cause offence by cross-posting. Actually, I only thought of LLTV after the initial post to LTS. I can/will delete the LTS forum post if anyone objects.

1 Like

Ugh, truncated script. Can you copy-paste it in here using


I don’t feel like reading it on github.

It is night now, I felt like opening new web pages.

Here is the source, for anyone not interested to go there.


function promptPassphrase {
	while [ -z "$PASS" ]; do
		read -s -p "Passphrase: " PASS
		echo ""
	while [ -z "$PASSCONF" ]; do
		read -s -p "Confirm passphrase: " PASSCONF
		echo ""
	echo ""

function getPassphrase {
	while [ "$PASS" != "$PASSCONF" ]; do
		echo "Passphrases did not match, try again..."

if [[ $UID -ne 0 ]]; then
 echo "This script needs to be run as root (with sudo)."
 exit 1

# Get the admin user.
users=($(ls /home))
echo "Existing users:"
for index in ${!users[*]}
	echo -e "\t[$index]: " ${users[$index]}

while [ -z "$SELECTION" ]; do read -p "Please select the user you created during the Ubuntu installation: " SELECTION; done
if [ -z "$ADMINUSER" ]; then
	echo "Invalid user selected. Please run the script again."

# Get the username for the primary user.
echo "Please enter a username for the primary device user that will be created by this script."
while [ -z "$ENDUSER" ]; do read -p "Username for primary device user: " ENDUSER; done
if [ -d "/home/$ENDUSER" ]; then
	if [ "$ENDUSER" == "$ADMINUSER" ]; then
		echo "Primary user cannot be the same as the admin user."

	read -p "The username you entered already exists. Do you want to continue? [y/n]: " CONFIRM
	if [ "$CONFIRM" != "y" ]; then

echo "If you are not using the default internet repositories you should configure this before running this script."
echo "You should also have an active network connection to the repositories."
read -p "Continue? [y/n]: " CONFIRM
if [ "$CONFIRM" != "y" ]; then

echo -e "${HIGHLIGHT}Running system updates...${NC}"
# Update.
apt-get update
# Upgrade.
apt-get dist-upgrade -y
# Remove packages.
apt-get remove -y popularity-contest
# And install required packages.
apt-get install -y apparmor-profiles apparmor-utils auditd 

# Configuring mount and grub. We need to make sure the script is running for the first time.
echo -e "${HIGHLIGHT}Configuring fstab...${NC}"
read -p "Is this the first time you run the post-install script? [y/n]: " CONFIRM
if [ "$CONFIRM" == "y" ]; then
	# Update fstab.
	echo -e "${HIGHLIGHT}Writing fstab config...${NC}"
	sed -ie '/\s\/home\s/ s/defaults/defaults,noexec,nosuid,nodev/' /etc/fstab
	EXISTS=$(grep "/tmp/" /etc/fstab)
	if [ -z "$EXISTS" ]; then
		echo "none /tmp tmpfs rw,noexec,nosuid,nodev 0 0" >> /etc/fstab
		sed -ie '/\s\/tmp\s/ s/defaults/defaults,noexec,nosuid,nodev/' /etc/fstab
	echo "none /run/shm tmpfs rw,noexec,nosuid,nodev 0 0" >> /etc/fstab
	# Bind /var/tmp to /tmp to apply the same mount options during system boot
 	echo "/tmp /var/tmp none bind 0 0" >> /etc/fstab
	# Temporarily make the /tmp directory executable before running apt-get and remove execution flag afterwards. This is because
	# sometimes apt writes files into /tmp and executes them from there.
	echo -e "DPkg::Pre-Invoke{\"mount -o remount,exec /tmp\";};\nDPkg::Post-Invoke {\"mount -o remount /tmp\";};" >> /etc/apt/apt.conf.d/99tmpexec
	chmod 644 /etc/apt/apt.conf.d/99tmpexec

# Set grub password.
echo -e "${HIGHLIGHT}Configuring grub...${NC}"
echo "Please enter a grub sysadmin passphrase..."

echo "set superusers=\"sysadmin\"" >> /etc/grub.d/40_custom
echo -e "$PASS\n$PASS" | grub-mkpasswd-pbkdf2 | tail -n1 | awk -F" " '{print "password_pbkdf2 sysadmin " $7}' >> /etc/grub.d/40_custom
sed -ie '/echo "menuentry / s/echo "menuentry /echo "menuentry --unrestricted /' /etc/grub.d/10_linux
sed -ie '/^GRUB_CMDLINE_LINUX_DEFAULT=/ s/"$/ module.sig_enforce=yes"/' /etc/default/grub
echo "GRUB_SAVEDEFAULT=false" >> /etc/default/grub

# Set permissions for admin user's home directory.
chmod 700 "/home/$ADMINUSER"

# Configure automatic updates.
echo -e "${HIGHLIGHT}Configuring automatic updates...${NC}"
EXISTS=$(grep "APT::Periodic::Update-Package-Lists" /etc/apt/apt.conf.d/20auto-upgrades)
if [ -z "$EXISTS" ]; then
	sed -i '/APT::Periodic::Update-Package-Lists/d' /etc/apt/apt.conf.d/20auto-upgrades
	echo "APT::Periodic::Update-Package-Lists \"1\";" >> /etc/apt/apt.conf.d/20auto-upgrades

EXISTS=$(grep "APT::Periodic::Unattended-Upgrade" /etc/apt/apt.conf.d/20auto-upgrades)
if [ -z "$EXISTS" ]; then
	sed -i '/APT::Periodic::Unattended-Upgrade/d' /etc/apt/apt.conf.d/20auto-upgrades
	echo "APT::Periodic::Unattended-Upgrade \"1\";" >> /etc/apt/apt.conf.d/20auto-upgrades

EXISTS=$(grep "APT::Periodic::AutocleanInterval" /etc/apt/apt.conf.d/10periodic)
if [ -z "$EXISTS" ]; then
	sed -i '/APT::Periodic::AutocleanInterval/d' /etc/apt/apt.conf.d/10periodic
	echo "APT::Periodic::AutocleanInterval \"7\";" >> /etc/apt/apt.conf.d/10periodic

chmod 644 /etc/apt/apt.conf.d/20auto-upgrades
chmod 644 /etc/apt/apt.conf.d/10periodic

# Prevent standard user executing su.
echo -e "${HIGHLIGHT}Configure su execution...${NC}"
dpkg-statoverride --update --add root adm 4750 /bin/su

# Protect user home directories.
echo -e "${HIGHLIGHT}Configuring home directories and shell access...${NC}"
sed -ie '/^DIR_MODE=/ s/=[0-9]*\+/=0700/' /etc/adduser.conf
sed -ie '/^UMASK\s\+/ s/022/077/' /etc/login.defs

read -p "User shell configuration:
[0] Set shell to /bin/passwd (allows password changes, bypassing Gnome bug) (default)
[1] Set shell to /sbin/nologin (prevents shell access)
[2] Set shell to /bin/bash (allows bash shell access)
	if [ "$LEVEL" == "1" ]; then
		# Disable shell access for new users (not affecting the existing admin user).
		sed -ie '/^SHELL=/ s/=.*\+/=\/usr\/sbin\/nologin/' /etc/default/useradd
		sed -ie '/^DSHELL=/ s/=.*\+/=\/usr\/sbin\/nologin/' /etc/adduser.conf
	elif [ "$LEVEL" == "2" ]; then
		# Keep shell access for new users (not affecting the existing admin user).
		sed -ie '/^SHELL=/ s/=.*\+/=\/bin\/bash/' /etc/default/useradd
		sed -ie '/^DSHELL=/ s/=.*\+/=\/bin\/bash/' /etc/adduser.conf	
		# Keep shell access for new users to prevent Gnome bug re. passwords (not affecting the existing admin user). Default option.
		sed -ie '/^SHELL=/ s/=.*\+/=\/bin\/passwd/' /etc/default/useradd
		sed -ie '/^DSHELL=/ s/=.*\+/=\/bin\/passwd/' /etc/adduser.conf		

# Installing libpam-pwquality 
echo -e "${HIGHLIGHT}Configuring minimum password requirements...${NC}"
apt-get install -f libpam-pwquality

# Create the standard user.
adduser "$ENDUSER"

# Set some AppArmor profiles to enforce mode.
echo -e "${HIGHLIGHT}Configuring apparmor...${NC}"
aa-enforce /etc/apparmor.d/usr.bin.firefox
aa-enforce /etc/apparmor.d/usr.sbin.avahi-daemon
aa-enforce /etc/apparmor.d/usr.sbin.dnsmasq
aa-enforce /etc/apparmor.d/
aa-enforce /etc/apparmor.d/usr.sbin.rsyslogd

# Setup auditing.
echo -e "${HIGHLIGHT}Configuring system auditing...${NC}"
if [ ! -f /etc/audit/rules.d/tmp-monitor.rules ]; then
echo "# Monitor changes and executions within /tmp
-w /tmp/ -p wa -k tmp_write
-w /tmp/ -p x -k tmp_exec" > /etc/audit/rules.d/tmp-monitor.rules

if [ ! -f /etc/audit/rules.d/admin-home-watch.rules ]; then
echo "# Monitor administrator access to /home directories
-a always,exit -F dir=/home/ -F uid=0 -C auid!=obj_uid -k admin_home_user" > /etc/audit/rules.d/admin-home-watch.rules
systemctl restart auditd.service

# Configure the settings for the "Welcome" popup box on first login.
echo -e "${HIGHLIGHT}Configuring user first login settings...${NC}"
mkdir -p "/home/$ENDUSER/.config"
echo yes > "/home/$ENDUSER/.config/gnome-initial-setup-done"
chown -R "$ENDUSER:$ENDUSER" "/home/$ENDUSER/.config"
sudo -H -u "$ENDUSER" ubuntu-report -f send no

# Disable error reporting services
echo -e "${HIGHLIGHT}Configuring error reporting...${NC}"
systemctl stop apport.service
systemctl disable apport.service
systemctl mask apport.service

systemctl stop whoopsie.service
systemctl disable whoopsie.service
systemctl mask whoopsie.service

if [ ! -f "/etc/dconf/profile/user" ]; then
	touch /etc/dconf/profile/user

EXISTS=$(grep "user-db:user" /etc/dconf/profile/user)
if [ -z "$EXISTS" ]; then
	echo "user-db:user" >> /etc/dconf/profile/user

EXISTS=$(grep "system-db:local" /etc/dconf/profile/user)
if [ -z "$EXISTS" ]; then
	echo "system-db:local" >> /etc/dconf/profile/user

# Optionally disable bluetooth
read -p "Do you want to disable bluetooth for all users? [y/n]: " CONFIRM
if [ "$CONFIRM" == "y" ]; then
systemctl disable bluetooth.service

# Lockdown Gnome screensaver lock settings
echo -e "${HIGHLIGHT}Configuring Gnome screensaver lock settings...${NC}"
mkdir -p /etc/dconf/db/local.d/locks
echo "[org/gnome/login-screen]


ubuntu-lock-on-suspend=true" > /etc/dconf/db/local.d/00_custom-lock

echo "/org/gnome/desktop/session/idle-delay
/org/gnome/login-screen/disable-user-list" > /etc/dconf/db/local.d/locks/00_custom-lock

# Remove user list from login page
sed -ie '/^\# disable-user-list\=true/ s/#//' /etc/gdm3/greeter.dconf-defaults

read -p "Do you want to disable lockscreen notifications? [y/n]: " CONFIRM
if [ "$CONFIRM" == "y" ]; then
echo "

banner-message-enable=false" >> /etc/dconf/db/local.d/00_custom-lock

echo "/org/gnome/desktop/notifications/show-in-lock-screen
/org/gnome/login-screen/banner-message-enable" >> /etc/dconf/db/local.d/locks/00_custom-lock


# Optionally Disable Location Services
read -p "Do you want to disable location services? [y/n]: " CONFIRM
if [ "$CONFIRM" == "y" ]; then
echo "
enabled=false" >> /etc/dconf/db/local.d/00_custom-lock

echo "/org/gnome/system/location/max-accuracy-level
/org/gnome/system/location/enabled" >> /etc/dconf/db/local.d/locks/00_custom-lock

# Further Privacy Setting
echo "
report-technical-problems=false" >> /etc/dconf/db/local.d/00_custom-lock
echo "/org/gnome/desktop/privacy/report-technical-problems" >> /etc/dconf/db/local.d/locks/00_custom-lock

# Optionally Set USB Restrictions (located within org/gnome/desktop/privacy)
read -p "Do you want to restrict USB usage? [y/n]: " CONFIRM
if [ "$CONFIRM" == "y" ]; then
echo "usb-protection=true" >> /etc/dconf/db/local.d/00_custom-lock
read -p "Set the restriction level:
[0] block USB on lockscreen (default)
[1] always block USB
	if [ "$LEVEL" == "1" ]; then
		echo "Setting USB lockdown mode to: always"
		echo "usb-protection-level='always'" >> /etc/dconf/db/local.d/00_custom-lock
		echo "/org/gnome/desktop/privacy/usb-protection-level" >> /etc/dconf/db/local.d/locks/00_custom-lock
		if [ ! -f "/etc/modprobe.d/blacklist.conf" ]; then
			touch /etc/modprobe.d/blacklist.conf
		if [ ! -f "/etc/rc.local" ]; then
			touch /etc/rc.local
			echo "#!/bin/bash" >> /etc/rc.local
		echo "blacklist usb_storage
blacklist uas" >> /etc/modprobe.d/blacklist.conf
		echo "modprobe -r uas
modprobe -r usb_storage" >> /etc/rc.local
		rmmod usb_storage
		echo "Setting USB lockdown mode to: lockscreen"
		echo "usb-protection-level='lockscreen'" >> /etc/dconf/db/local.d/00_custom-lock
		echo "/org/gnome/desktop/privacy/usb-protection-level" >> /etc/dconf/db/local.d/locks/00_custom-lock

dconf update

# Fix dconf permissions, otherwise option locks don't apply upon subsequent script executions
chmod 644 -R /etc/dconf/db/
chmod a+x /etc/dconf/db/local.d/locks
chmod a+x /etc/dconf/db/local.d
chmod a+x /etc/dconf/db

# Disable apport (error reporting)
sed -ie '/^enabled=1$/ s/1/0/' /etc/default/apport

sudo -H -u "$ENDUSER" dbus-launch gsettings set com.ubuntu.update-notifier show-apport-crashes false

# Fix some permissions in /var that are writable and executable by the standard user.
echo -e "${HIGHLIGHT}Configuring additional directory permissions...${NC}"
chmod o-w /var/crash
chmod o-w /var/metrics
chmod o-w /var/tmp

# Setting up firewall without any rules.
echo -e "${HIGHLIGHT}Configuring firewall…  ${NC}"
ufw enable	

echo -e "${HIGHLIGHT}Installation complete.${NC}"

read -p "Reboot now? [y/n]: " CONFIRM
if [ "$CONFIRM" == "y" ]; then

The script:

  • verifies that you are the root user, or running it with sudo, or quits
  • it gets the users from the /home directory (kinda inefficient, because your users may have other locations, like /opt or /export/home, better to get it from /etc/passwd and see what users have id starting with 1000 and up)
  • shows you the list of users and asks you to insert the number for the user which you used when installing ubuntu
  • asks you to insert a username as an end user, that is not the admin user (for security purposes, you’re not supposed to use an admin account for your daily activities, you’re supposed to su - into it, then sudo)
  • if the username exists, asks for confirmation if you’re sure
  • runs updates and installs apparmor
  • it creates some tmpfs mountpoints in fstab like /tmp and bind mounts /tmp to /var/tmp (which I find dumb, they should be different - the idea of /var/tmp being a tmpfs is fine, just it shouldn’t be combined with /tmp)
  • reads a pasphrase twice and makes sure it’s not empty
  • if the passphrases didn’t match, asks again until you get it right
  • sets that password for grub in order to allow boot probably (if your system is not encrypted, this doesn’t do anything than maybe prevent end-users from adding a boot flag to boot to rescue mode and reset the root password, but even then, if booting other OS is allowed in the bios, this doesn’t do anything either)
  • configures automatic, unattended updates
  • shows a prompt for each user from /home to set their shells accordingly (passwd, nologin or bash)
  • installs require to set better passwords and set expiration policies
  • adds some apparmor profiles
  • enables some audits for changes and executions from /tmp
  • I believe it monitors admin access over /home directories, probably to prevent admin abuse, I suppose
  • configures first login and sets the gnome ubuntu-reporting thing to no by default, so you bypass the automatic data sending to canonical
  • disables error reporting services (idk why, doesn’t sound good to me when you want to debug stuff, although many exploits are done through error reporting software)
  • sets some dconf profiles
  • asks if you want to disable bluetooth
  • sets the gnome screen locking to 10 minutes of idle (I would go for 5 if I really wanted to lock down things) and sets suspend on lockon
  • disables the user list in the login screen (you have to type it manually)
  • asks if you want lock screen notifications
  • asks if you want location services
  • sets privacy settings
  • asks if you want to block usb devices (blocking people from copying files to and from thumb drives)
  • removes write permissions for others to /var/{crash,metrics,tmp} (why would they do it for /var/tmp and /tmp, for /tmp it makes no sense, which is the reason why /tmp shouldn’t be bind mounted to /var/tmp)
  • asks to reboot

Decent little script, especially the apparmor stuff, but you can do without it. It seems mostly for desktop use anyway (with all the gnome and firefox garbage policies). I wouldn’t use it on a server, but maybe I’d implement some of the stuff, like pam and apparmor, or rather, selinux, because I prefer rhel-based systems, even fedora, to ubuntu or suse that use apparmor - just preference. But if I was to really be able to pick a server os that should be rock-solid and secure, I’d choose Alpine diskless install and keep things minimal, although I’d have to use apparmor.


@ThatGuyB, my apologies. I did not include the code explicitly, but the link to the GitHub page caused the forum software to auto-add the extract. I agree that GitHub formatting is not so easy on the eye. I see you have added the script below, and a synopsis of what its doing - my thanks for the nice contributiont that I think others will find very useful.

I have been trying to find out more about apparmour, as someone who is not familiar with it. I will start here:

I have often installed Linux with my usual username, and continue to use it thereafter and sudo to run something privileged. The note you made about not daily driving with the installation user causes me concern - so I will look in to that. I do create keys to ssh to a host (i.e. not using a password, as they are not secure) but as soon as I need to use sudo on the remote I have to enter the password. Is there a better (more secure) way to authenticate in a terminal?

My goal is to develop a Linux operational and configuration security policy for my business. I would like to have a credable response to a challenge that I do not install antivirus for my Linux servers, as is the norm for Windows. Steve Gibson has commented on the fact that, for Windows at least, antimalware vendors have often reduced system security and created instability due to the nature of what they are doing - by hooking the OS at a deep level - and Microsoft has struggled and reacted to control delinquent third-party antimalware services.

Steve Gibson: Not so fun is the growing problem that we are seeing with third-party antivirus, at least on Windows. And we’ve talked about the cause of this. The cause is that today’s AV, in order to continue to demonstrate its value to its customers, really has to hook its claws deeply into Windows. Microsoft now is arguably competing. We’re seeing instances where third-party AV is causing Microsoft trouble. It’s always been sort of controversial because, in order to get the access that antivirus software needs, it needs essentially to kind of be a rootkit. It needs to go in and make modifications to the underlying kernel level code in order to essentially insert taps into Windows for the things it wants to do. Well, the problem is, you know, I said it was rootkit-like. Well, Windows is trying to prevent rootkits. So, I mean, there really is a fundamental schism here which we’re now increasingly reporting. And it happened again last Tuesday, a week ago, in an interesting way.

I think I will experiment with the useful parts of the script - your analysis is very useful! Thanks.

PS. Does Jay have any past content to help me?

Well… you could technically use a 2FA in the terminal too, like a fido2 key or TOTP, but that’s a hassle. Easiest way to keep things secure:

  • edit sshd config to only allow certain users to authenticate via ssh keys. Disabling password authentication does the trick most of the time though
  • when you ssh to a server, ssh to a non-sudoer, then su - into the sudoer and run your sudo commands
  • try to limit su - and su - root to prevent people from logging in as the root user. It is more convenient and if you know what you are doing, there’s nothing wrong with it, but it’s better you have them run commands with sudo and monitor their activity
  • if you aren’t using an AAA server, have each sudoer account have a unique, RNG long password and for sharing the password, use VaultWarden, or if you can afford it, Bitwarden (they both use the same GUIs and plugins / extensions, but vaultwarden is FOSS and gratis, while I believe bitwarden has some proprietary bits and peaces and has or used to have a requirement on MS SQL server, which was bonkers, which is why I went with vaultwarden, or bitwarden_rs as it was used to be named, until I think the dev was asked to change the name to avoid trademark reasons)

The Enterprise Linux Security series (53 episodes and counting) is great. It’s not a technical tutorial or review but more of a conversation about things to consider. Sometimes it’s best to get an overview of things before reaching out for one specific tool, at least this helps me when I’m trying to find things online or ask questions about specific topics.


What does this syntax mean? Expand items in the array?

If you put it into context:

for index in ${!users[*]}

it appears to be an array of items, made into a list (kind of like “cat users.txt” or “index in [1…29]”. I never used something like this, I prefer to use files, but I suppose bash array variables aren’t that uncommon. Files are just easier to work with, but sometimes keeping things in RAM is more efficient (unless you just use /tmp, which normally is a tmpfs anyway, then you only get a slight overhead of writing the file to a RAM FS, instead of directly to RAM, but it’s not that big for small files).


array=(one two three)

# ${!array[@]} is the list of all the indexes set in the array
for i in ${!array[@]}; do
  echo "$i, ${array[$i]}"
1 Like