So after many test and reading documentation about the syntax for apparmor i finally create some apparmor profiles specially for Qubes to improve the security, privacy and anonymity. dkzkz/apparmor-qubes: Apparmor profile for debian template should work on whonix and kicksecure template - Codeberg.org
There is a bug in the apparmor package shipped by debian on the stable version i opened a issue about that Apparmor 4.1.0-1 in Qubes OS Profile does not conform to protocol error (#592) · Issues · AppArmor / apparmor · GitLab this bug can lead to security issue according to the dev. And it will not be fixed soon by debian so we will use the unstable version of apparmor to get the last fix and features. I will try to ask the Qubes dev to make every debian templat use the unstable version of apparmor in the future instead of the stable version
I didn’t get any issue by using the unstable version it works far better than the stable version.
“What’s the main advantage of using those apparmor profiles ?”
By default every apps who reach internet can’t read the machine-id of the users so it should spoof the fact you’re using Qubes but they can detect that in a other way
By default every apps can’t access to the filesystem of the users (except file-manager because they do not have internet access i removed that so it’s ok to let them access this i think)
By default apps can’t read and write in every home directories including QubesIncoming they can only access to the Downloads folder
By default you will NOT be able to use extension like keepassxc or bitwarden and a lot of others extensions in the Brave-browser and probably the other browser too because those extension need to use /bin command in order to work correctly and i didn’t add those commands. ublock is working fine. Why i’ve done that ? Because EVERY extensions are insecure read this post and also this one if you take a look at the Mullvad-Browser they removed the abilitie to easily install extensions in their Browser.
- Start your debian template
- I recommend to use the kicksecure repository to get their apparmor package or you could also use the kicksecure template on Qubes
- Now do those commands
# 1. Add the unstable repository
sudo sh -c 'cat <<EOF >>/etc/apt/sources.list
deb https://deb.debian.org/debian/ unstable main
EOF'
# 2. Create the pinning file – high priority for the AppArmor packages,
# very low priority for everything else from the unstable suite
sudo sh -c 'cat <<EOF >/etc/apt/preferences.d/unstable-pin
# ---- AppArmor packages (high priority) ----
Package: apparm* python*-appar* python*-libappar*
Pin: release a=unstable
Pin-Priority: 990
# ---- All other packages from unstable (very low priority) ----
Package: *
Pin: release a=unstable
Pin-Priority: 1
EOF'
# 3. Refresh the package indexes
sudo apt update
# 4. Install (or upgrade) the AppArmor packages from the unstable repository
sudo apt install -y apparmor apparmor-utils apparmor-profiles apparmor-profiles-extra
The command earlier will setup the unstable debian repository and it will install only the package needed to run apparmor.
echo "Types: deb
URIs: https://deb.kicksecure.com
Suites: trixie
Components: main contrib non-free
Enabled: yes
Signed-By: /usr/share/keyrings/derivative.asc" | sudo tee /etc/apt/sources.list.d/derivative.sources
To get the apparmor profiles from kicksecure
echo "Types: deb
URIs: tor+https://deb.kicksecure.com
Suites: trixie
Components: main contrib non-free
Enabled: yes
Signed-By: /usr/share/keyrings/derivative.asc" | sudo tee /etc/apt/sources.list.d/derivative.sources
You could use their onion repository here but updating packages in the future will be very slow.
By using the tor repository you will need sys-whonix or the tor package installed in your system to install their packages.
- Shutdown the template
In order to make apparmor works correctly in Qubes you need to run this commands in dom0 :
qvm-prefs x kernelopts "swiotlb=2048 security=apparmor"
Replace “x” by the name of your template
- Start your templateVM open a terminal and confirm apparmor is running by doing as root
sudo aa-enabled
It must say “Yes” in that case you can continue the guide otherwise you have done something wrong.
-
I provide a python3 GUI script to automatically install the apparmor profiles from my repository you have the choice betweeen a local installation or a internet installation you will need “git” installed on you system if you pick the internet script. Or you can also unpack the release tar.gz file and do it manually here
-
Do
sudo apt-get -y install git
installation.py
#!/usr/bin/env python3
import os
import sys
import subprocess
import shutil
import pathlib
import getpass
from tkinter import (
Tk, Toplevel, StringVar, BooleanVar, Listbox, Scrollbar,
Entry, Frame, messagebox
)
from tkinter import ttk
# -------------------------------------------------
# Configuration (mirrors the Bash variables)
# -------------------------------------------------
BASE_DIR = pathlib.Path("/home/user")
REPO_URL = "https://codeberg.org/dkzkz/apparmor-qubes"
REPO_DIR = BASE_DIR / "repo"
TARGET_DIRS = [
"stable", "browser", "email", "xdg-open", "metadata", "multimedia",
"password-manager", "selfhost", "utilities", "com",
"torrent-gui", "display-vm", "qubes-scripts",
"file-manager", "monero"
]
SKIP_EXT = {"md", "py" "sh", "png", "jpeg", "jpg"}
SKIP_PATTERNS = {"*.sample", "*.bak", "README*"}
SKIP_DIRS = {"install", "beta-profile", "screenshot"}
HIDE = {""}
NO_CHOICE = {"open"}
AUTO_VLC = {"vlc", "vlc-network"}
# -------------------------------------------------
# Global reference to the Tk root (used for abort)
# -------------------------------------------------
ROOT = None # will be set in __main__
# -------------------------------------------------
# Helper utilities
# -------------------------------------------------
def run_cmd(cmd, check=False, capture=False, env=None):
"""Thin wrapper around subprocess.run."""
result = subprocess.run(
cmd,
shell=True,
check=check,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
env=env,
)
if capture:
return result.stdout.decode().strip()
return None
def sudo_check():
"""Abort if the script is not run via sudo."""
if os.geteuid() != 0:
messagebox.showerror(
"Permission Denied",
"This script must be run with sudo privileges."
)
sys.exit(1)
def get_x11_info():
"""Determine the X‑user, DISPLAY and XAUTHORITY."""
try:
xuser = run_cmd(
"who | awk '{ if ($2 ~ /^:/) { print $1; exit } }'",
capture=True,
)
except Exception:
xuser = ""
if not xuser:
try:
xuser = run_cmd(
"getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 { print $1; exit }'",
capture=True,
)
except Exception:
xuser = ""
if not xuser:
xuser = os.getenv("SUDO_USER") or getpass.getuser()
uhome = pathlib.Path(os.path.expanduser(f"~{xuser}"))
display = run_cmd(
"ps e | grep -Pz 'DISPLAY=:[0-9]+' | grep -oP 'DISPLAY=\\K:[0-9]+' | head -n1",
capture=True,
)
display = display or ":0"
xauth = uhome / ".Xauthority"
return xuser, uhome, display, xauth
def run_as_user(cmd, xuser, display, xauth):
"""Execute a command as the X‑user with proper env."""
env = os.environ.copy()
env.update({"DISPLAY": display, "XAUTHORITY": str(xauth)})
return run_cmd(f"sudo -u {xuser} {cmd}", env=env)
def skip_file(path: pathlib.Path) -> bool:
"""Return True if the file should be ignored."""
name = path.name
if name in HIDE:
return True
if name.startswith("."):
return True
if path.suffix.lstrip(".") in SKIP_EXT:
return True
for pat in SKIP_PATTERNS:
if pathlib.Path(name).match(pat):
return True
return False
def purge_vlc_profiles(root: Tk):
"""Ask user and delete any /etc/apparmor.d/vlc* files."""
if not messagebox.askyesno(
"Purge VLC Profiles",
"This will remove all existing VLC‑related profiles. Continue?",
parent=root,
):
return
for f in pathlib.Path("/etc/apparmor.d").glob("vlc*"):
try:
f.unlink()
except PermissionError:
pass
messagebox.showinfo("VLC Profiles", "All VLC profiles have been purged.", parent=root)
def enforce_profile(src_file: pathlib.Path, enforce_cmd: str, root: Tk):
"""Copy the profile to /etc/apparmor.d and enforce it with a progress bar."""
fname = src_file.name
if fname in AUTO_VLC:
purge_vlc_profiles(root)
dst = pathlib.Path("/etc/apparmor.d") / fname
# ----- move the file (still running as root) -----
try:
if dst.exists():
dst.unlink()
shutil.move(str(src_file), str(dst))
except Exception as e:
messagebox.showerror(
"Enforce error",
f"Failed to move {src_file} → {dst}:\n{e}",
parent=root,
)
return
# ----- progress window (no OK button) -----
prog_win = Toplevel(root)
prog_win.title("Enforcing Profile")
prog_win.resizable(False, False)
ttk.Label(prog_win, text=f"Enforcing {fname}…").pack(padx=10, pady=(10, 5))
bar = ttk.Progressbar(prog_win, mode="indeterminate", length=300)
bar.pack(padx=10, pady=5)
bar.start(10)
prog_win.update()
# enforce **as root**
run_cmd(f"sudo {enforce_cmd} {dst}", check=False)
bar.stop()
prog_win.destroy()
def abort_installation():
"""Remove cloned repo (if any), close the GUI, and exit."""
if REPO_DIR.exists():
shutil.rmtree(REPO_DIR)
global ROOT
if ROOT is not None:
try:
ROOT.destroy()
except Exception:
pass
sys.exit(0)
class ModernApp:
def __init__(self, master):
self.master = master
master.title("AppArmor Installer")
master.geometry("460x180")
master.resizable(False, False)
ttk.Style().theme_use("clam")
ttk.Label(master, text="AppArmor profile installer",
font=("Helvetica", 14)).pack(pady=12)
master.after(100, self.start)
# -------------------------------------------------
# 1️⃣ Template‑mode dialog
# -------------------------------------------------
def ask_template_mode(self):
dlg = Toplevel(self.master)
dlg.title("Template Check")
dlg.resizable(False, False)
ttk.Label(dlg, text="Is this script running in a template?").pack(padx=20, pady=10)
answer = BooleanVar(value=False)
def set_yes():
answer.set(True)
dlg.destroy()
def set_no():
answer.set(False)
dlg.destroy()
btns = Frame(dlg)
btns.pack(pady=8)
ttk.Button(btns, text="Yes", command=set_yes).grid(row=0, column=0, padx=5)
ttk.Button(btns, text="No", command=set_no).grid(row=0, column=1, padx=5)
# Cancel → abort the whole installation
ttk.Button(
btns,
text="Cancel",
command=lambda: (dlg.destroy(), abort_installation()),
).grid(row=0, column=2, padx=5)
dlg.wait_window()
return answer.get()
# -------------------------------------------------
# 2️⃣ Enforcement‑method dialog
# -------------------------------------------------
def choose_enforce_method(self):
dlg = Toplevel(self.master)
dlg.title("Enforcement method")
dlg.resizable(False, False)
ttk.Label(dlg, text="Select the enforcement command:").pack(padx=20, pady=10)
choice = StringVar()
combo = ttk.Combobox(
dlg,
textvariable=choice,
state="readonly",
values=[
"aa-enforce (default)",
"apparmor_parser -r (loads profile in the kernel)",
],
)
combo.current(0)
combo.pack(padx=20, pady=5)
btn_frame = Frame(dlg)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="OK", command=dlg.destroy).grid(row=0, column=0, padx=5)
# Cancel → abort the whole installation
ttk.Button(
btn_frame,
text="Cancel",
command=lambda: (dlg.destroy(), abort_installation()),
).grid(row=0, column=1, padx=5)
dlg.wait_window()
method = "apparmor_parser -r" if "parser" in choice.get() else "aa-enforce"
if method == "apparmor_parser -r":
messagebox.showwarning(
"Removal Warning",
"DO NOT remove a profile by using \"rm -f /etc/apparmor.d/x\".\n"
"You must use \"sudo apparmor_parser -R /etc/apparmor.d/x\" before removing a profile.",
parent=self.master,
)
return method
# -------------------------------------------------
# 3️⃣ Profile‑selection dialog
# -------------------------------------------------
def select_profiles_dialog(self, top, files):
"""
Returns:
None – user cancelled → abort installation
"ALL" – user pressed “Select ALL displayed”
set() – indices of chosen files
"""
dlg = Toplevel(self.master)
dlg.title(f"Profiles in {top}")
dlg.geometry("820x560")
dlg.resizable(True, True)
search_var = StringVar()
ttk.Label(dlg, text="Filter profiles:").pack(anchor="w", padx=10, pady=(10, 0))
ttk.Entry(dlg, textvariable=search_var).pack(fill="x", padx=10, pady=5)
frame = Frame(dlg)
frame.pack(fill="both", expand=True, padx=10, pady=5)
sb = Scrollbar(frame)
sb.pack(side="right", fill="y")
lb = Listbox(frame, selectmode="extended", yscrollcommand=sb.set,
font=("Helvetica", 10))
lb.pack(fill="both", expand=True)
sb.config(command=lb.yview)
select_all_var = BooleanVar(value=False)
def toggle_select_all():
if select_all_var.get():
lb.select_set(0, "end")
else:
lb.select_clear(0, "end")
ttk.Checkbutton(
dlg,
text="Select ALL displayed",
variable=select_all_var,
command=toggle_select_all,
).pack(pady=5)
def refresh_list(*_):
term = search_var.get().lower()
lb.delete(0, "end")
for idx, p in enumerate(files):
name = p.name
if not term or term in name.lower():
lb.insert("end", f"{idx+1}. {name}")
search_var.trace_add("write", refresh_list)
refresh_list()
result = {"value": None}
def ok():
sel = {
int(lb.get(i).split(".", 1)[0]) - 1
for i in lb.curselection()
}
result["value"] = "ALL" if select_all_var.get() else sel
dlg.destroy()
def cancel():
dlg.destroy()
abort_installation() # abort instead of just returning
btns = Frame(dlg)
btns.pack(pady=10)
ttk.Button(btns, text="OK", command=ok).grid(row=0, column=0, padx=8)
ttk.Button(btns, text="Cancel", command=cancel).grid(row=0, column=1, padx=8)
dlg.wait_window()
return result["value"]
# -------------------------------------------------
# 4️⃣ Process a single top‑level directory
# -------------------------------------------------
def process_directory(self, top, enforce_cmd):
src_dir = REPO_DIR / top
if not src_dir.is_dir():
return
candidates = sorted(p for p in src_dir.rglob("*") if p.is_file())
filtered = [p for p in candidates if not skip_file(p)]
if not filtered:
messagebox.showinfo(
"No Profiles",
f"No enforceable files in {top} – skipping.",
parent=self.master,
)
return
remaining = []
for f in filtered:
if f.name in NO_CHOICE:
enforce_profile(f, enforce_cmd, self.master)
else:
remaining.append(f)
if not remaining:
messagebox.showinfo(
"Auto Enforcement",
f"All profiles in {top} were automatically enforced – moving to next directory.",
parent=self.master,
)
return
while remaining:
choice = self.select_profiles_dialog(top, remaining)
# If the user hit Cancel, `select_profiles_dialog` already called
# abort_installation(), so execution never reaches here.
if choice is None: # safety net
abort_installation()
if choice == "ALL":
for p in remaining:
enforce_profile(p, enforce_cmd, self.master)
remaining.clear()
break
selected_files = [remaining[i] for i in choice]
# Special handling for nautilus/thunar and prefix‑matching
extra = []
for src in selected_files:
if src.name.lower() in {"nautilus", "thunar"}:
extra.extend([p for p in remaining if p.name.startswith("qubes")])
else:
prefix = src.name.split("-")[0]
extra.extend(
[p for p in remaining
if p.name.startswith(prefix) and p not in selected_files]
)
to_enforce = list(dict.fromkeys(selected_files + extra))
for p in to_enforce:
enforce_profile(p, enforce_cmd, self.master)
if p in remaining:
remaining.remove(p)
if not remaining:
messagebox.showinfo(
"Profiles Enforced",
f"All profiles in {top} have been enforced – moving to next directory.",
parent=self.master,
)
break
# -------------------------------------------------
# 5️⃣ Main workflow
# -------------------------------------------------
def start(self):
if self.ask_template_mode():
os.environ["all_proxy"] = "http://127.0.0.1:8082/"
global XUSER, UHOME, DISPLAY, XAUTHORITY
XUSER, UHOME, DISPLAY, XAUTHORITY = get_x11_info()
if not XUSER or not DISPLAY:
messagebox.showerror(
"X11 Error",
"Unable to determine X11 display or user. Cannot continue.",
parent=self.master,
)
sys.exit(1)
# add Xauthority entry (ignore errors)
run_as_user(
f"xauth add $(sudo -u {XUSER} xauth -f {XAUTHORITY} list | tail -1)",
XUSER,
DISPLAY,
XAUTHORITY,
)
enforce_cmd = self.choose_enforce_method()
# ----- clean previous clone if present -----
if REPO_DIR.exists():
shutil.rmtree(REPO_DIR)
# ----- clone repo with progress bar -----
prog = Toplevel(self.master)
prog.title("Cloning repository")
prog.resizable(False, False)
ttk.Label(prog, text="Cloning repository…").pack(padx=20, pady=10)
bar = ttk.Progressbar(prog, mode="indeterminate", length=300)
bar.pack(padx=20, pady=10)
bar.start(10)
prog.update()
try:
run_cmd(f"git clone {REPO_URL} {REPO_DIR}", check=True)
except subprocess.CalledProcessError as e:
prog.destroy()
messagebox.showerror(
"Clone failed",
f"Git clone failed:\n{e}",
parent=self.master,
)
abort_installation()
bar.stop()
prog.destroy()
# ----- clean up possible conflicts -----
for usr_bin in pathlib.Path("/etc/apparmor.d").glob("usr.bin.*"):
plain = usr_bin.name.split(".", 2)[-1]
plain_path = pathlib.Path("/etc/apparmor.d") / plain
if plain_path.is_file():
usr_bin.unlink()
# ----- process each directory -----
for top in TARGET_DIRS:
self.process_directory(top, enforce_cmd)
# ----- final clean‑up -----
if REPO_DIR.exists():
shutil.rmtree(REPO_DIR)
messagebox.showinfo(
"Installation Complete",
"All selected profiles have been installed and enforced.",
parent=self.master,
)
self.master.destroy()
if __name__ == "__main__":
sudo_check()
root = Tk()
root.withdraw() # hide the empty root window; dialogs are modal
ROOT = root # store global reference for abort_installation()
ModernApp(root)
root.mainloop()
localinstallation.py
#!/usr/bin/env python3
import os
import sys
import subprocess
import shutil
import pathlib
import getpass
from tkinter import (
Tk, Toplevel, StringVar, BooleanVar, Listbox, Scrollbar,
Entry, Frame, messagebox
)
from tkinter import ttk
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent # directory that contains this script
DIR_PREFIX = "apparmor" # look for dirs named/starting with this
TARGET_DIRS = [] # will hold the found directories
SKIP_EXT = {"md", "py", "sh", "png", "jpeg", "jpg"}
SKIP_PATTERNS = {"*.sample", "*.bak", "README*"}
SKIP_DIRS = {"install", "beta-profile", "screenshot"}
HIDE = {""}
NO_CHOICE = {"open"}
AUTO_VLC = {"vlc", "vlc-network"}
ROOT = None # global reference for abort_installation()
def run_cmd(cmd, check=False, capture=False, env=None):
"""Thin wrapper around subprocess.run."""
result = subprocess.run(
cmd,
shell=True,
check=check,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
env=env,
)
if capture:
return result.stdout.decode().strip()
return None
def sudo_check():
"""Abort if the script is not run via sudo."""
if os.geteuid() != 0:
messagebox.showerror(
"Permission denied",
"This script must be run with sudo privileges."
)
sys.exit(1)
def get_x11_info():
"""Determine the X‑user, DISPLAY and XAUTHORITY."""
try:
xuser = run_cmd(
"who | awk '{ if ($2 ~ /^:/) { print $1; exit } }'",
capture=True,
)
except Exception:
xuser = ""
if not xuser:
try:
xuser = run_cmd(
"getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 { print $1; exit }'",
capture=True,
)
except Exception:
xuser = ""
if not xuser:
xuser = os.getenv("SUDO_USER") or getpass.getuser()
uhome = pathlib.Path(os.path.expanduser(f"~{xuser}"))
display = run_cmd(
"ps e | grep -Pz 'DISPLAY=:[0-9]+' | grep -oP 'DISPLAY=\\K:[0-9]+' | head -n1",
capture=True,
)
display = display or ":0"
xauth = uhome / ".Xauthority"
return xuser, uhome, display, xauth
def run_as_user(cmd, xuser, display, xauth):
"""Execute a command as the X‑user with proper env."""
env = os.environ.copy()
env.update({"DISPLAY": display, "XAUTHORITY": str(xauth)})
return run_cmd(f"sudo -u {xuser} {cmd}", env=env)
def skip_file(path: pathlib.Path) -> bool:
"""Return True if the file should be ignored."""
name = path.name
if name in HIDE:
return True
if name.startswith("."):
return True
if path.suffix.lstrip(".") in SKIP_EXT:
return True
for pat in SKIP_PATTERNS:
if pathlib.Path(name).match(pat):
return True
return False
def purge_vlc_profiles(root: Tk):
"""Ask user and delete any /etc/apparmor.d/vlc* files."""
if not messagebox.askyesno(
"Purge VLC Profiles",
"This will remove all existing VLC‑related profiles. Continue?",
parent=root,
):
return
for f in pathlib.Path("/etc/apparmor.d").glob("vlc*"):
try:
f.unlink()
except PermissionError:
pass
messagebox.showinfo("VLC Profiles", "All VLC profiles have been purged.", parent=root)
def enforce_profile(src_file: pathlib.Path, enforce_cmd: str, root: Tk):
"""Copy the profile to /etc/apparmor.d and enforce it with a progress bar."""
fname = src_file.name
if fname in AUTO_VLC:
purge_vlc_profiles(root)
dst = pathlib.Path("/etc/apparmor.d") / fname
try:
if dst.exists():
dst.unlink()
shutil.move(str(src_file), str(dst))
except Exception as e:
messagebox.showerror(
"Enforce error",
f"Failed to move {src_file} → {dst}:\n{e}",
parent=root,
)
return
prog_win = Toplevel(root)
prog_win.title("Enforcing Profile")
prog_win.resizable(False, False)
ttk.Label(prog_win, text=f"Enforcing {fname}…").pack(padx=10, pady=(10, 5))
bar = ttk.Progressbar(prog_win, mode="indeterminate", length=300)
bar.pack(padx=10, pady=5)
bar.start(10)
prog_win.update()
run_cmd(f"sudo {enforce_cmd} {dst}", check=False)
bar.stop()
prog_win.destroy()
def clean_apparmor_dirs():
"""Delete every directory that was added to TARGET_DIRS."""
for d in TARGET_DIRS:
try:
shutil.rmtree(d)
except Exception:
pass
TARGET_DIRS.clear()
def abort_installation():
"""Close the GUI, clean up, and exit."""
clean_apparmor_dirs()
global ROOT
if ROOT is not None:
try:
ROOT.destroy()
except Exception:
pass
sys.exit(0)
class ModernApp:
def __init__(self, master):
self.master = master
master.title("AppArmor Installer")
master.geometry("460x180")
master.resizable(False, False)
ttk.Style().theme_use("clam")
ttk.Label(master, text="AppArmor profile installer",
font=("Helvetica", 14)).pack(pady=12)
master.after(100, self.start)
def choose_enforce_method(self):
dlg = Toplevel(self.master)
dlg.title("Enforcement method")
dlg.resizable(False, False)
ttk.Label(dlg, text="Select the enforcement command:").pack(padx=20, pady=10)
choice = StringVar()
combo = ttk.Combobox(
dlg,
textvariable=choice,
state="readonly",
values=[
"aa-enforce (default)",
"apparmor_parser -r (loads profile in the kernel)",
],
)
combo.current(0)
combo.pack(padx=20, pady=5)
btn_frame = Frame(dlg)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="OK", command=dlg.destroy).grid(row=0, column=0, padx=5)
ttk.Button(
btn_frame,
text="Cancel",
command=lambda: (dlg.destroy(), abort_installation()),
).grid(row=0, column=1, padx=5)
dlg.wait_window()
method = "apparmor_parser -r" if "parser" in choice.get() else "aa-enforce"
if method == "apparmor_parser -r":
messagebox.showwarning(
"Removal Warning",
"DO NOT remove a profile by using \"rm -f /etc/apparmor.d/x\".\n"
"You must use \"sudo apparmor_parser -R /etc/apparmor.d/x\" before removing a profile.",
parent=self.master,
)
return method
def select_profiles_dialog(self, top, files):
dlg = Toplevel(self.master)
dlg.title(f"Profiles in {top}")
dlg.geometry("820x560")
dlg.resizable(True, True)
search_var = StringVar()
ttk.Label(dlg, text="Filter profiles:").pack(anchor="w", padx=10, pady=(10, 0))
ttk.Entry(dlg, textvariable=search_var).pack(fill="x", padx=10, pady=5)
frame = Frame(dlg)
frame.pack(fill="both", expand=True, padx=10, pady=5)
sb = Scrollbar(frame)
sb.pack(side="right", fill="y")
lb = Listbox(frame, selectmode="extended", yscrollcommand=sb.set,
font=("Helvetica", 10))
lb.pack(fill="both", expand=True)
sb.config(command=lb.yview)
select_all_var = BooleanVar(value=False)
def toggle_select_all():
if select_all_var.get():
lb.select_set(0, "end")
else:
lb.select_clear(0, "end")
ttk.Checkbutton(
dlg,
text="Select ALL displayed",
variable=select_all_var,
command=toggle_select_all,
).pack(pady=5)
def refresh_list(*_):
term = search_var.get().lower()
lb.delete(0, "end")
for idx, p in enumerate(files):
name = p.name
if not term or term in name.lower():
lb.insert("end", f"{idx+1}. {name}")
search_var.trace_add("write", refresh_list)
refresh_list()
result = {"value": None}
def ok():
sel = {
int(lb.get(i).split(".", 1)[0]) - 1
for i in lb.curselection()
}
result["value"] = "ALL" if select_all_var.get() else sel
dlg.destroy()
def cancel():
dlg.destroy()
abort_installation()
btns = Frame(dlg)
btns.pack(pady=10)
ttk.Button(btns, text="OK", command=ok).grid(row=0, column=0, padx=8)
ttk.Button(btns, text="Cancel", command=cancel).grid(row=0, column=1, padx=8)
dlg.wait_window()
return result["value"]
def process_directory(self, top, enforce_cmd):
src_dir = pathlib.Path(top)
if not src_dir.is_dir():
return
candidates = sorted(p for p in src_dir.rglob("*") if p.is_file())
filtered = [p for p in candidates if not skip_file(p)]
if not filtered:
messagebox.showinfo(
"No Profiles",
f"No enforceable files in {top} – skipping.",
parent=self.master,
)
return
remaining = []
for f in filtered:
if f.name in NO_CHOICE:
enforce_profile(f, enforce_cmd, self.master)
else:
remaining.append(f)
if not remaining:
messagebox.showinfo(
"Auto Enforcement",
f"All profiles in {top} were automatically enforced – moving to next directory.",
parent=self.master,
)
return
while remaining:
choice = self.select_profiles_dialog(top, remaining)
if choice is None: # safety net
abort_installation()
if choice == "ALL":
for p in remaining:
enforce_profile(p, enforce_cmd, self.master)
remaining.clear()
break
selected_files = [remaining[i] for i in choice]
# Prefix‑matching logic (same as original)
extra = []
for src in selected_files:
if src.name.lower() in {"nautilus", "thunar"}:
extra.extend([p for p in remaining if p.name.startswith("qubes")])
else:
prefix = src.name.split("-")[0]
extra.extend(
[p for p in remaining
if p.name.startswith(prefix) and p not in selected_files]
)
to_enforce = list(dict.fromkeys(selected_files + extra))
for p in to_enforce:
enforce_profile(p, enforce_cmd, self.master)
if p in remaining:
remaining.remove(p)
if not remaining:
messagebox.showinfo(
"Profiles Enforced",
f"All profiles in {top} have been enforced – moving to next directory.",
parent=self.master,
)
break
def start(self):
global XUSER, UHOME, DISPLAY, XAUTHORITY
XUSER, UHOME, DISPLAY, XAUTHORITY = get_x11_info()
if not XUSER or not DISPLAY:
messagebox.showerror(
"X11 Error",
"Unable to determine X11 display or user. Cannot continue.",
parent=self.master,
)
sys.exit(1)
run_as_user(
f"xauth add $(sudo -u {XUSER} xauth -f {XAUTHORITY} list | tail -1)",
XUSER,
DISPLAY,
XAUTHORITY,
)
enforce_cmd = self.choose_enforce_method()
for entry in SCRIPT_DIR.iterdir():
if entry.is_dir() and entry.name.startswith(DIR_PREFIX):
TARGET_DIRS.append(str(entry))
if not TARGET_DIRS:
messagebox.showerror(
"No AppArmor directories",
f'No sub‑directory starting with "{DIR_PREFIX}" found in {SCRIPT_DIR}.',
parent=self.master,
)
abort_installation()
for top in TARGET_DIRS:
self.process_directory(top, enforce_cmd)
clean_apparmor_dirs()
messagebox.showinfo(
"Installation Complete",
"All selected profiles have been installed and enforced.",
parent=self.master,
)
self.master.destroy()
if __name__ == "__main__":
sudo_check()
root = Tk()
root.withdraw() # hide the empty root window; dialogs are modal
ROOT = root # store global reference for abort_installation()
ModernApp(root)
root.mainloop()
!! BEFORE INSTALLING THE APPARMOR PROFILE INSTALL THE APP FIRST !! When you install the app debian overwrite the apparmor profile of the app. But not when the app update itself. So that mean if you run the script and you install the app then the apparmor profile from the repository will be removed automatically by debian (i don’t know if it’s normal or not)
To use the scripts do python3.py (name of the script)
I’ve made the installation process fast and easy if you click on “firefox-glx” it will automatically enforce every firefox files. It’s the same thing for Mullvad-Browser , brave etc…
If you click on “Nautilus” or “Thunar” it will automatically install every qubes files it’s needed to use qvm-convert , qvm-convert-img etc…
It should take 20 seconds to install.
F.A.Q
“Do Qubes copy-vm menu entry works ?”
Yes i’ve tried and it works without any issue
“Can i use firejail with your profiles ?”
You shouldn’t do it in fact using firejail will only increase the possibility of an attack the creator of Firejail has said himself this
Don't use this on enterprise servers, or any other multiuser system. Firejail was built for single-user desktops.
Maybe you could use firejail with my apparmor profiles but i didn’t test to see if it works and i will never do it because i’ve tested firejail for a long time with Qubes and some applications wasn’t properly starting with firejail so it was running without any protection which make firejail useless for example Nautilus wasn’t launching with firejail so i have to find a trick to force Nautilus to run under firejail which is frustrating to do. There is a good reason why Tails , Whonix or Secureblue do not rely on firejail to secure a system they rely only on Apparmor or Selinux or Secureblue.