Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Created July 8, 2026 09:10
Show Gist options
  • Select an option

  • Save ssokolow/4c9364c1d1697ea46622459d85864c63 to your computer and use it in GitHub Desktop.

Select an option

Save ssokolow/4c9364c1d1697ea46622459d85864c63 to your computer and use it in GitHub Desktop.
A Python script to ease generating strict Firejail profiles for GOG.com games and the like
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Firejail Profile+Launcher Generator for Games"""
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__appname__ = "Sandboxify"
__version__ = "0.1"
__license__ = "MIT"
import logging
import os
from typing import Optional
log = logging.getLogger(__name__)
SITE_RULES_PATH = os.path.expanduser(
"~/.config/firejail/sandboxify-common.inc")
# TODO: Add an --option for games which need access to either a CD/DVD/BD drive
# or CDEmu.
# TODO: Design options for easing strace use (maybe --strace=actual/binary/path
# which also sets the relevant allow flags)
# TODO: Add a --dns option which can be --dns or --dns=<IP> and document it as
# an addition to --network which adds a nameserver to /etc/resolv.conf so
# games can contact their developer/publisher where, without it, you tend
# to get a *de facto* "LAN games are OK and IP direct connect is OK, but
# the Internet is unavailable otherwise".
# TODO: Decide how to offer an option for `private-etc ssl/certs` since you'll
# probably want it if you're using --dns, but not necessarily, since
# there may be situations where denying a game a set of valid roots will
# break matchmaking or update-checking while still allowing DynDNS-based
# domains to be typed into direct IP connection boxes.
# TODO: Integrate what I learned from setting up Saints Row 3 so I can have
# presets to add to Wine for things like DXVK, Wine Virtual Desktop,
# and generating the base start.sh harness and wineprefix folder from
# templates.
# TODO: Audit /etc/firejail/disable-common.inc to determine whether it should
# be `include`'d or excerpted.
# TODO: Explore how compatible disable-proc.inc is with games
# TODO: Think about options for making the LAN less visible with --network
PROFILE_TMPL = """
# Enable the usual blanket sandboxing directives
caps.drop all
ipc-namespace
nodbus
nodvd # Remove this if you're playing a game from physical media
nogroups
nonewprivs
noprinters
noroot
notv
nou2f
novideo
seccomp%(seccomp_extra)s
restrict-namespaces
# NOTE: See /etc/firejail/disable-common.inc if removing any of these
private-tmp
private-dev
private-etc %(private_etc)s
private-bin %(private_bin)s
# Give the game its own private $HOME for userdata
private %(game_home)s
# Only grant the game access to its install directory
# and hide and lock down the contents of the sandboxed launch script
whitelist%(gamedir_ro_suffix)s %(game_root)s
blacklist %(run_path)s
# Remove environment variables that may aid exploits at worst and point
# programs at things the sandbox blocks at best
rmenv BROWSER
rmenv CARGO_REGISTRY_TOKEN
rmenv DOCKER_HOST
rmenv EDITOR
rmenv GPG_AGENT_INFO
rmenv GH_TOKEN
rmenv GH_ENTERPRISE_TOKEN
rmenv GITHUB_TOKEN
rmenv GITHUB_ENTERPRISE_TOKEN
rmenv GTK_RC_FILES
rmenv GTK2_RC_FILES
rmenv KONSOLE_DBUS_SERVICE
rmenv KONSOLE_DBUS_SESSION
rmenv MAIL
rmenv MAILER
rmenv MAILPATH
rmenv MANPAGER
rmenv PAGER
rmenv PAM_KWALLET5_LOGIN
rmenv RESTIC_KEY_HINT
rmenv RESTIC_PASSWORD_COMMAND
rmenv RESTIC_PASSWORD_FILE
rmenv SESSION_MANAGER
rmenv SSH_ASKPASS
rmenv SSH_AGENT_PID
rmenv SSH_AUTH_SOCK
rmenv TCLLIBPATH
rmenv VISUAL
# NOTE: DBUS_{SESSION,SYSTEM}_BUS_ADDRESS are omitted because `nodbus` would
# override them anyway.
# Cut /usr/share down to things games have been legitimately observed to need
# NOTE: May crash the game if something tries to open a GTK file chooser dialog
whitelist-ro /usr/share/alsa
whitelist-ro /usr/share/libdrm
whitelist-ro /usr/share/locale
whitelist-ro /usr/share/pipewire
whitelist-ro /usr/share/terminfo
whitelist-ro /usr/share/vulkan
whitelist-ro /usr/share/vulkansc
whitelist-ro /usr/share/X11/locale
whitelist-ro /usr/share/X11/xkb
blacklist /boot
blacklist /opt
blacklist /snap
blacklist /usr/include
blacklist /usr/libexec
blacklist /usr/local
blacklist /usr/src
blacklist /var
blacklist /proc/config.gz
read-only /lib
read-only /usr
# Borrow the bits from Firejail 0.9.72's diable-common.inc which appear to
# still not be covered by my much more whitelist-based approach
blacklist /.fscrypt
blacklist /.snapshots
blacklist /initrd*
blacklist /vmlinuz*
blacklist /run/timeshift
blacklist /usr/lib/chromium/chrome-sandbox
blacklist /usr/lib/dbus-1.0/dbus-daemon-launch-helper
blacklist /usr/lib/eject/dmcrypt-get-device
blacklist /usr/lib/openssh
blacklist /usr/lib/opera/opera_sandbox
blacklist /usr/lib/policykit-1/polkit-agent-helper-1
blacklist /usr/lib/squid/basic_pam_auth
blacklist /usr/lib/ssh
blacklist /usr/lib/snapd
blacklist /usr/lib/vmware
blacklist /usr/lib/virtualbox
blacklist /usr/lib/xorg/Xorg.wrap
blacklist /usr/lib64/virtualbox
blacklist ${RUNUSER}/*.lock
blacklist ${RUNUSER}/*.slave-socket
blacklist ${RUNUSER}/.dbus-proxy
blacklist ${RUNUSER}/.flatpak
blacklist ${RUNUSER}/.flatpak-cache
blacklist ${RUNUSER}/.flatpak-helper
blacklist ${RUNUSER}/app
blacklist ${RUNUSER}/containers
blacklist ${RUNUSER}/crun
blacklist ${RUNUSER}/doc
blacklist ${RUNUSER}/gnome-session-leader-fifo
blacklist ${RUNUSER}/gnome-shell
blacklist ${RUNUSER}/gsconnect
blacklist ${RUNUSER}/inaccessible
blacklist ${RUNUSER}/kdeinit5__*
blacklist ${RUNUSER}/kdesud_*
#?HAS_NODBUS: blacklist ${RUNUSER}/ksocket-*
blacklist ${RUNUSER}/libpod
blacklist ${RUNUSER}/libvirt
blacklist ${RUNUSER}/pk-debconf-socket
blacklist ${RUNUSER}/runc
blacklist ${RUNUSER}/snapd-session-agent.socket
blacklist ${RUNUSER}/systemd
blacklist ${RUNUSER}/toolbox
blacklist ${RUNUSER}/update-notifier.pid
# Games generally need `unix` to create a window and `netlink` for gamepads
# Network multiplayer uses `inet` and `inet6`
protocol unix,netlink%(extra_protos)s
%(extra_commands)s
# Include for any site-specific customizations (eg. special /mnt rules)
# or upgrades to the ruleset
include %(site_rules_path)s
# vim: set ft=conf :
"""
RUN_TMPL = """
#!/bin/sh
cd "$(dirname "$(readlink -f "$0")")" || exit
firejail --profile="%(profile_path)s" %(extra_run_args)s %(game_cmd)s
"""
def home_ify(path: str) -> str:
home = os.path.expanduser('~')
path = os.path.abspath(path)
if path.startswith(home):
return os.path.join('${HOME}', os.path.relpath(path, home))
return path
def process_arg(path: str, network: bool, electron: bool,
game_name: Optional[str], apparmor: bool, lib32: bool, alsa_conf: bool,
memory_deny_write_execute: bool, writable_game_dir: bool,
etc_passwd: bool, wine: bool, crackle_fix: Optional[int] = None
) -> None:
extra_commands = []
extra_run_args = []
private_bin = []
path = os.path.realpath(path)
if not os.path.isfile(path) and os.access(path, os.X_OK):
log.critical("Not an executable file: %s", path)
return
if '\n' in path:
log.critical(
"Don't know how to escape paths containing newlines: %r", path)
return
if path.endswith('.AppImage'):
extra_run_args.append('--appimage')
if path.endswith('.sh'):
# Commands needed by...
# GOG.com's start.sh (at least two different versions)
private_bin.extend(['cat', 'chmod', 'basename', 'bash',
'dirname', 'env', 'grep', 'head', 'sh', 'tail', 'uname'])
# Neverwinter Nights's launch.sh
private_bin.append('readlink')
# Things like SMAPI
private_bin.append('xterm')
# Support using /mnt/... paths for game install locations
if path.startswith('/mnt/'):
extra_commands.extend(['blacklist /media', 'blacklist /run/media'])
else:
extra_commands.append('disable-mnt')
if apparmor:
extra_commands.append('apparmor')
if crackle_fix:
extra_commands.append('env PULSE_LATENCY_MSEC=%d' % crackle_fix)
private_etc = []
if alsa_conf:
private_etc.append('alsa/conf.d') # only needed for Windward so far
if etc_passwd:
private_etc.append('passwd')
if not lib32:
extra_commands.append('blacklist /lib32')
extra_commands.append('blacklist /usr/lib32')
if memory_deny_write_execute:
extra_commands.append('memory-deny-write-execute')
if wine:
extra_commands.append('whitelist-ro /usr/share/wine')
private_bin.extend(['wine', 'winecfg', 'cut'])
if network:
msg = ('If you have issues joining network games, try adding '
'`dns 1.1.1.1`')
print(msg + " to the game's Firejail profile")
extra_commands.append('# ' + msg)
extra_protos = ',inet,inet6'
else:
extra_commands.append('net none # Deny network to single-player game')
extra_protos = ''
# TODO: Look into ways to optionally make game_root-relative paths portable
# TODO: Options to manually specify game name and root directory
# TODO: If the game executable is a shell script, chmod -x it and invoke it
# by directly calling the shebang command
# TODO: Proper use of XDG Base Directory Spec
game_root = os.path.dirname(path)
if game_name is None:
game_name = os.path.basename(game_root).replace(' ', '_')
game_home = os.path.join(os.path.expanduser("~/.local/share"),
game_name)
profile_path = os.path.join(os.path.expanduser("~/.config/firejail"),
game_name)
run_path = os.path.join(game_root, 'run.sh')
# TODO: Do this properly:
game_cmd = './' + os.path.basename(path)
if game_cmd == './start.sh':
game_cmd = 'bash ./start.sh'
os.chmod(path, 0o644) # Prevent accidentally launching unsandboxed
print("Profile will be written to: ", profile_path)
print("Run script will be written to: ", run_path)
print("Game will store its user data in: ", game_home)
response = ''
while response not in ['y', 'n']:
response = input("Continue [yn]? ").lower()[:1]
if response != 'y':
return
# TODO: Detect pre-existing profiles, run scripts, and private home dirs
with open(profile_path, 'w') as fobj:
fobj.write(PROFILE_TMPL.strip() % {
# TODO: Write a template netfilter config which allows game-hosting
'extra_commands': '\n'.join(extra_commands),
'extra_protos': extra_protos,
'gamedir_ro_suffix': '' if writable_game_dir else '-ro',
'game_home': home_ify(game_home),
'game_root': game_root,
'private_bin': ','.join(private_bin) if private_bin else 'none',
'private_etc': ','.join(private_etc) if private_etc else 'none',
'run_path': run_path,
'seccomp_extra': ' !chroot' if electron else '',
'site_rules_path': SITE_RULES_PATH,
})
with open(run_path, 'w') as fobj:
fobj.write(RUN_TMPL.strip() % {
'extra_run_args': ' '.join(extra_run_args),
'game_cmd': game_cmd,
'profile_path': home_ify(profile_path),
})
if not os.path.exists(game_home):
os.makedirs(game_home)
if not os.path.exists(SITE_RULES_PATH):
parent = os.path.dirname(SITE_RULES_PATH)
if not os.path.exists(parent):
os.makedirs(parent)
with open(SITE_RULES_PATH, 'w') as fobj:
fobj.write("# Put your custom firejail rules for all games here")
os.chmod(run_path, 0o755)
print("OK. You should now be able to run %s to launch the game." % run_path)
def main() -> None:
"""The main entry point, compatible with setuptools entry points."""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
parser.add_argument('-V', '--version', action='version',
version="%%(prog)s v%s" % __version__)
parser.add_argument('-v', '--verbose', action="count",
default=2, help="Increase the verbosity. Use twice for extra effect.")
parser.add_argument('-q', '--quiet', action="count",
default=0, help="Decrease the verbosity. Use twice for extra effect.")
parser.add_argument('--allow-memory-write-execute', action="store_true",
default=False, help="Disable forced hardware DEP. Try this if game "
"fails to start. (OpenAL and .NET seem to require this.)")
parser.add_argument('--alsa-conf', action="store_true", default=False,
help="Allow access to /etc/alsa/conf.d (Needed by Windward. Try if you "
"have no audio.)")
parser.add_argument('--crackle-fix', action="store", nargs='?', default=60,
metavar='MSEC', help="Set PULSE_LATENCY_MSEC to the provided value "
"(default if provided without argument: %(default)s)")
parser.add_argument('--electron', action="store_true", default=False,
help="Loosen seccomp filter for compatibility with Electron apps")
parser.add_argument('--etc-passwd', action="store_true", default=False,
help="Allow access to /etc/passwd (required by games like Neverwinter "
"Nights EE, Stellaris, and The Escapists which use calls like "
"getpwuid/getpwnam instead of building their userdata path from "
"$HOME)")
parser.add_argument('--lib32', action="store_true", default=False,
help="Don't blacklist /usr/lib32 (for 32-bit-only games)")
parser.add_argument('--network', action="store_true", default=False,
help="Allow Internet access for multiplayer gaming")
parser.add_argument('--no-apparmor', action="store_true", default=False,
help="Disable the protection most likely to break things. Re-run with "
"this option if the game fails to start.")
parser.add_argument('--writable-game-dir', action="store_true",
default=False, help="Allow game to modify its own install directory "
"instead of just its userdata in its sandboxed $HOME.")
parser.add_argument('--wine', action="store_true",
default=False, help="Allow access to system Wine install")
parser.add_argument('--game-name', action="store", default=None,
metavar='NAME', help="Override the autodetected name for the game")
parser.add_argument('path', action="store", nargs="+",
help="Command to be run inside the sandbox to start the game")
# Reminder: %(default)s can be used in help strings.
args = parser.parse_args()
# Set up clean logging to stderr
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
args.verbose = max(args.verbose, 0)
logging.basicConfig(level=log_levels[args.verbose],
format='%(levelname)s: %(message)s')
for path in args.path:
process_arg(path, network=args.network, electron=args.electron,
game_name=args.game_name, apparmor=not args.no_apparmor,
lib32=args.lib32, alsa_conf=args.alsa_conf,
memory_deny_write_execute=not args.allow_memory_write_execute,
writable_game_dir=args.writable_game_dir,
etc_passwd=args.etc_passwd, wine=args.wine,
crackle_fix=args.crackle_fix)
print("\nWhile the generated profiles have been designed to work with a "
"wide variety of games, it's possible to find the odd game that does "
"something extra unusual. If the game crashes on startup, use "
"`--allow-debuggers` in the `run.sh` firejail arguments and `strace` "
"at the beginning of the line to launch the actual binary (not "
"something like `start.sh` or `launch.sh`) to diagnose the problem. "
"(eg. Neverwinter Nights Enhanced Edition turned out to be blindly "
"trusting it could query non-sensitive information about the user "
"account and required `private-etc passwd`.")
if __name__ == '__main__': # pragma: nocover
main()
# vim: set sw=4 sts=4 expandtab :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment