Linux Equivalent to Group Policy
I’ve been working on taking back my data (from Microsoft) by building out my business around Linux. But, the big question I get every time I talk about it is ‘What about Group Policy?’. Of course, Group Policy is the name of a Microsoft product, and you wouldn’t try to manage your ChromeOS fleet with it, so why try to force it on Linux too? Fundamentally, it’s just a method of pushing registry and configuration to machines, which we can already do on Linux with packages or automation, but of course the Linux desktop is not a clone of Windows, so what configuration should we push? Today, I investigate that question, with 7 tricks to manage your Linux workstation environment!
Contents⌗
- Video
- Desktop Environment (dconf)
- Browser (firefox)
- Software Execution (fapolicy)
- Software Installation (polkit)
- USB Peripherals (usbguard)
- Removable Disks (udisks2)
- VPN Profile (network-manager)
Video⌗
Desktop Environment Settings (dconf)⌗
For this example, I’ve chosen to force the desktop to blank after 10 minutes, and require a password after that. So, I need to configure the settings in dconf, and then lock them down so they can’t be overridden by users.
First, the configuration (/etc/dconf/db/local.d/16-screen-lock):
[org/gnome/desktop/session]
# Blank the display after 10 minutes
idle-delay=uint32 600
[org/gnome/desktop/screensaver]
# Lock essentially immediately when the screen blanks
lock-delay=uint32 5
# Require authentication to unlock
lock-enabled=true
[org/gnome/desktop/lockdown]
# Don't allow users to disable the lock screen entirely
disable-lock-screen=false
Next, a lockdown, preventing the user from changing these settings (/etc/dconf/db/local.d/locks/16-screen-lock):
/org/gnome/desktop/session/idle-delay
/org/gnome/desktop/screensaver/lock-delay
/org/gnome/desktop/screensaver/lock-enabled
/org/gnome/desktop/lockdown/disable-lock-screen
One quirk I’ve noticed is the GNOME Settings panel appears to let you modify these settings, but then it doesn’t save them if they are locked down. So, users might notice that changing their settings don’t appear to apply, and there is no warning that it fails because these settings are managed by the organization. Also don’t forget to run dconf update to apply the configuration changes.
Browser Settings (Firefox)⌗
Firefox can be highly managed by a policies.json file, so our organization will write one. Firefox supports quite a lot of enterprise policies
{
"policies": {
//Block config pages
"BlockAboutConfig": true,
"BlockAboutAddons": true,
"BlockAboutProfiles": true,
//Block private browsing
"PrivateBrowsingModeAvailability": 1,
//Remove (don't auto-create) Firefox's defaults
"NoDefaultBookmarks": true,
//Add corporate bookmarks
"ManagedBookmarks": [
{
"toplevel_name": "Corporate"
},
{
"url": "apalrd.net",
"name": "Public Website"
},
{
"name": "Internal Business Bullshit",
"children": [
{
"url": "https://mozilla.org",
"name": "Mozilla.org"
},
{
"url": "https://support.mozilla.org/",
"name": "Mozilla Support"
}
]
}
],
//Disable SetDesktopBackground
"DisableSetDesktopBackground": true,
//Corporate IT support menu
"SupportMenu": {
"Title": "Corporate Support",
"URL": "http://apalrd.net/myip/"
},
//Don't allow users to accept cert errors
"DisableSecurityBypass": {
"InvalidCertificate": true,
"SafeBrowsing": true
},
//Managed corporate extensions
"ExtensionSettings": {
//Block extensions by default
"*": {
"installation_mode": "blocked"
},
//Force install Privacy Badger from EFF (it's pretty good)
"jid1-MnnxcxisBPnSXQ@jetpack": {
"installation_mode": "force_installed",
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/privacy-badger17/latest.xpi"
}
//You can also allow install but not force, but we blocked the addon page anyway
}
}
}
Installing this one is pretty easy - just drop your policy in /etc/firefox/policies/policies.json and Firefox will respect it. Should be chmod 644, which is default in /etc/.
Software Execution (fapolicy)⌗
Just installing fapolicy gives you the default of only allowing execution of files wihch are trusted (i.e. installed via the package manager): sudo dnf install fapolicyd and sudo systemctl enable --now fapolicyd. The default is to only allow non-root users to execute trusted software (trust being established from RPM and Flatpak).
The best way to allow additional binaries is to package your enterprise software in RPMs and distribute them via the package manager, which makes them trusted.
But, if you have software development for example, and need to execute binaries, here’s a rule to allow software-devs to run any binaries in /devs (lol I was like why don't we allow devs to do dev in /dev?):
/etc/fapolicyd/rules.d/69-devs.rules
allow perm=execute gid=software-devs : dir=/devs/
Software Installation (polkit)⌗
We can also allow some users to install their own software via the GNOME Software app. Here’s the Polkit policy to allow this:
// /etc/polkit-1/rules.d/69-allow-sw-install.rules
polkit.addRule(function(action, subject) {
// Act on package-install
// Warning - This allows any dnf 'trusted' transaction
// 'trusted' means that the rpm's signature is verified
// This would still theoretically allow you to remove the kernel, etc.
// You probably do not want to grant this to most users
if (action.id == "org.rpm.dnf.v0.rpm.execute_trusted_transaction" &&
// Seat is on the local system (not ssh / ...)
subject.active && subject.local &&
// User is in group to allow software install
subject.isInGroup("software-installers")) {
return polkit.Result.YES;
}
// Act on Flatpak-install
// Flatpak has minimal ability to break the system
// so this is a safe option for 'normal' users
// worst case
if (action.id == "org.freedesktop.Flatpak.app-install" &&
// Seat is on the local system (not ssh / ...)
subject.active && subject.local &&
// User is in group to allow software install
subject.isInGroup("software-installers")) {
return polkit.Result.YES;
}
// Act on PackageKit
// This is a mediator for packages, which would normally be used
// by GNOME Software Center, KDE Plasma, ...
// but Fedora 44+ goes directly to dnf or flatpak now
// but if you are using another DE it may use Packagekit
if (action.id == "org.freedesktop.packagekit.package-install" &&
// Seat is on the local system (not ssh / ...)
subject.active && subject.local &&
// User is in group to allow software install
subject.isInGroup("software-installers")) {
return polkit.Result.YES;
}
});
If you want to control what software shows up in GNOME Software then you need to create your own repository for either rpm of flatpack or both, containing software versions which you allow users to self-install. I suggest starting by allowing flatpak self-installs only.
USB Peripherals (usbguard)⌗
For USBGuard, we need to install it (sudo dnf install usbguard and sudo systemctl enable --now usbguard), and if you want to see what’s attached, you can generate a rule which would allow anything currently attached to the system (sudo usbguard generate-policy). I did that and got this:
# USB root controllers
allow id 1d6b:0002 serial "0000:00:0d.0" name "xHCI Host Controller" hash "d3YN7OD60Ggqc9hClW0/al6tlFEshidDnQKzZRRk410=" parent-hash "Y1kBdG1uWQr5CjULQs7uh2F6pHgFb6VDHcWLk83v+tE=" with-interface 09:00:00 with-connect-type ""
allow id 1d6b:0003 serial "0000:00:0d.0" name "xHCI Host Controller" hash "G+G3Mro8zBWJavFOAQUtoNiOsZSfBCt2XqHfOufYFis=" parent-hash "Y1kBdG1uWQr5CjULQs7uh2F6pHgFb6VDHcWLk83v+tE=" with-interface 09:00:00 with-connect-type ""
allow id 1d6b:0002 serial "0000:00:14.0" name "xHCI Host Controller" hash "jEP/6WzviqdJ5VSeTUY8PatCNBKeaREvo2OqdplND/o=" parent-hash "rV9bfLq7c2eA4tYjVjwO4bxhm+y6GgZpl9J60L0fBkY=" with-interface 09:00:00 with-connect-type ""
allow id 1d6b:0003 serial "0000:00:14.0" name "xHCI Host Controller" hash "E8Zs26CP5+JQoiPVmDSuTb4j11VatW+WHlWxiX8+qJc=" parent-hash "rV9bfLq7c2eA4tYjVjwO4bxhm+y6GgZpl9J60L0fBkY=" with-interface 09:00:00 with-connect-type ""
# JetKVM composite devices
allow id 1d6b:0104 serial "" name "USB Emulation Device" hash "QqqayY7Hm9NAvwXmlF03Fe+H5ri62HeVKbmydKtApp0=" parent-hash "jEP/6WzviqdJ5VSeTUY8PatCNBKeaREvo2OqdplND/o=" via-port "3-6" with-interface { 03:01:01 03:00:02 03:01:02 03:00:00 01:01:00 01:02:00 01:02:00 08:06:50 } with-connect-type "hotplug"
# Intel BE200 Bluetooth
allow id 8087:0036 serial "" name "" hash "XwbcZSrllifsnXXcFkmww6DJnTpumS/N2rYZllwTvH4=" parent-hash "jEP/6WzviqdJ5VSeTUY8PatCNBKeaREvo2OqdplND/o=" via-port "3-8" with-interface { e0:01:01 e0:01:01 e0:01:01 e0:01:01 e0:01:01 e0:01:01 e0:01:01 e0:01:01 } with-connect-type "hardwired"
The rules can go in /etc/usbguard/rules.conf or /etc/usbguard/rules.d/*. Here’s the full docs for your reference, it’s very useful. Pro tip, you can run sudo usbguard generate-policy | sudo tee /etc/usbguard/rules.d/10-my-hw.conf to always allow your current hardware and ensure you don’t lock yourself our. One quirk I’ve found is that these (especially the hash) can sometimes change with kernel updates, locking you out. Start by using sudo systemctl start usbguard, do not enable it so you can reboot if you get locked out. When you are happy with your rules, then you can sudo systemctl enable usbguard so it starts on boot.
usbguard also has a bit of a quirk in that it requires the rules files to be chmod 600 (read/write by root, but nobody else), which goes against the usual conventions of /etc (which is usually 644 so normal users can read config files). So, you will need to sudo chmod -R 600 /etc/usbguard/rules.d/ followed by sudo chmod +x /etc/usbguard/rules.dto fix this - accidentally clearing the executable bit on the directory itself will cause usbguard to just … not read the directory … (since it can’t list it - executable means directory can be listed), and block every usb device. Oops.
But anyway here’s a simpler rule that you might like:
# /etc/usbguard/rules.d/69-apalrds-rules.conf
# Allow any USB HID device, including keyboards and mice.
# USB class 03 = Human Interface Device.
allow with-interface one-of { 03:*:* }
# Allow USB mass storage
# USB class 08
allow with-interface one-of { 08:*:* }
# Allow Yubikey
allow id 1040:0407
# Allow USB host-controller/root-hub devices.
# USB class 09 = Hub.
allow with-interface one-of { 09:*:* }
# Everything not matched above is implicitly blocked.
If you want to roll this out in your org, you’ll need a fairly good idea of what hardware you have, and use some combination of one-of / exact / .. to restrict keyboards / mice / .., plus all of the internal hardware (like wifi / bluetooth) you use, and things like YubiKeys.
Removable Disks (udisks2)⌗
While we could also restrict this with USBGuard (which would prevent the USB device from attaching at all), we have more granularity if we do it in Polkit. Mounting a filesystem requires privilages, so we can instead apply policy to the point where the block device is mounted as a filesystem, and consider who is trying to mount it (and not just that it is a USB device).
Anyway, here’s an example that only allows users in the storage-users group to use removable disks:
// /etc/polkit-1/rules.d/69-removable-storage.rules
polkit.addRule(function(action, subject) {
// Act on filesystem-mount actions
if (action.id == "org.freedesktop.udisks2.filesystem-mount" &&
// Seat is on the local system (not ssh / ...)
subject.local &&
// User is (not) in the storage-users group
!subject.isInGroup("storage-users")) {
return polkit.Result.NO;
}
});
VPN Profile (network-manager)⌗
Another thing we probably want to roll out to endpoints is VPN connections. I’d usually suggest OpenVPN or IPSec for this depending on if you are deploying machine certificates, but today I’ll test with bare Wireguard. So, we want users to be able to activate/deactivate the VPN on their own, but not modify it. We also still want them to be able to connect to wifi on their own, without admin permissions. So, we need them to be able to edit some connections, just not this connection.
First, though, let’s create the connection (/etc/NetworkManager/system-connections/Corporate VPN.nmconnection) (and obviously change these keys) (and also this must be chmod 600 or NetworkManager will ignore it) (and also do nmcli con reload afterwards or reboot)
[connection]
id=Corporate VPN
uuid=fe18f98b-3b30-4bb5-9b52-ada04c82bb47
type=wireguard
interface-name=wg-corp
autoconnect=false
[wireguard]
listen-port=51820
private-key=WOifjArnNjOSRUjH6Gu7T+VmPveI9bDg+gQLgSTCJmo=
[wireguard-peer.Ao3Rcwdfl73A3XA8tnIcnIQQd7FIL+Hug3l4bpWPNEQ=]
endpoint=[your_ip]:51820
allowed-ips=fd69:beef:cafe::/64
persistent-keepalive=25
[ipv4]
method=disabled
[ipv6]
method=manual
addresses=fd69:beef:cafe::2/64
addr-gen-mode=stable-privacy
Next, since we put this in the system-connections directory, that makes it different from a user connection, so we can restrict policy on it differently. Again, we’re going to Polkit here, and NetworkManager respects polkit. Our rule this time is (/etc/polkit-1/rules.d/67-networkmanager-corporate.rules). Note that this is the default for non-root users, so you only need this if you want to give the network-admin group permissions to edit it!
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.NetworkManager.settings.modify.system") {
if (subject.isInGroup("network-admin")) {
return polkit.Result.YES;
}
return polkit.Result.NO;
}
});