Skip to content

Instantly share code, notes, and snippets.

@xuxucode
Last active February 21, 2024 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xuxucode/e365d85cf2cc4e8a844b25b55e55a89f to your computer and use it in GitHub Desktop.
Save xuxucode/e365d85cf2cc4e8a844b25b55e55a89f to your computer and use it in GitHub Desktop.
Get latest `docker-default` AppArmor profile by parsing template.go
// Usage: node apparmor-docker-default.js
const path = require("node:path");
const fs = require("node:fs");
const profileDirectory = "/etc/apparmor.d"
/**
* @typedef ProfileData
* @property {string} name
* @property {string} daemonProfile
* @property {string[]} imports
* @property {string[]} innerImports
*/
// Output "docker-default" profile.
{
getDefault().then(profile => {
console.log(profile);
});
}
async function loadTemplate() {
const response = await fetch("https://raw.githubusercontent.com/moby/moby/master/profiles/apparmor/template.go");
const text = await response.text();
const startTag = "const baseTemplate = `";
const startIndex = text.indexOf(startTag) + startTag.length;
const endIndex = text.lastIndexOf("`");
return text.slice(startIndex, endIndex);
}
// https://github.com/moby/moby/blob/master/profiles/apparmor/apparmor.go#L59
async function getDefault(name = "docker-default") {
// Figure out the daemon profile.
let currentProfile = "";
try {
currentProfile = fs.readFileSync("/proc/self/attr/current", "utf8").trim();
} catch {
// If we couldn't get the daemon profile, assume we are running
// unconfined which is generally the default.
}
const daemonProfile = currentProfile.split(" ")[0] || "unconfined";
return generateDefault(name, daemonProfile)
}
// https://github.com/moby/moby/blob/master/profiles/apparmor/apparmor.go#L32
/**
* @param {string} name
* @param {string} daemonProfile
*/
async function generateDefault(name, daemonProfile) {
const template = await loadTemplate();
/** @type {ProfileData} */
const p = {
name,
daemonProfile,
imports: [],
innerImports: [],
};
if (macroExists("tunables/global")) {
p.imports.push("#include <tunables/global>");
} else {
p.imports.push("@{PROC}=/proc/");
}
if (macroExists("abstractions/base")) {
p.innerImports.push("#include <abstractions/base>");
}
return parseTemplate(template, p);
}
/**
* @param {string} template
* @param {ProfileData} p
*/
function parseTemplate(template, p) {
const endTag = "{{end}}";
// Replace "{{range $value := .Imports}}".
const importsStartIndex = template.indexOf("{{range $value := .Imports}}");
const importsEndIndex = template.indexOf(endTag, importsStartIndex) + endTag.length;
template = template.substring(0, importsStartIndex) + p.imports.join("\n") + template.substring(importsEndIndex);
// Replace "{{range $value := .InnerImports}}".
const innerImportsStartIndex = template.indexOf("{{range $value := .InnerImports}}");
const innerImportsEndIndex = template.indexOf(endTag, innerImportsStartIndex) + endTag.length;
template = template.substring(0, innerImportsStartIndex) + p.innerImports.map(i => " " + i).join("\n") + template.substring(innerImportsEndIndex);
// Replace "{{.Name}}".
template = template.replaceAll(/\{\{\.Name\}\}/g, p.name);
// Replace "{{.DaemonProfile}}"
template = template.replaceAll(/\{\{\.DaemonProfile\}\}/g, p.daemonProfile);
return template;
}
// https://github.com/moby/moby/blob/master/profiles/apparmor/apparmor.go#L52
function macroExists(m) {
return fs.existsSync(path.join(profileDirectory, m));
}
@xuxucode
Copy link
Author

xuxucode commented Feb 16, 2024

Output on Ubuntu:

#include <tunables/global>

profile docker-default flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  network,
  capability,
  file,
  umount,
  # Host (privileged) processes may send signals to container processes.
  signal (receive) peer=unconfined,
  # dockerd may send signals to container processes (for "docker kill").
  signal (receive) peer=unconfined,
  # Container processes may send signals amongst themselves.
  signal (send,receive) peer=docker-default,

  deny @{PROC}/* w,   # deny write for all files directly in /proc (not in a subdir)
  # deny write to files not in /proc/<number>/** or /proc/sys/**
  deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9/]*}/** w,
  deny @{PROC}/sys/[^k]** w,  # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel)
  deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w,  # deny everything except shm* in /proc/sys/kernel/
  deny @{PROC}/sysrq-trigger rwklx,
  deny @{PROC}/kcore rwklx,

  deny mount,

  deny /sys/[^f]*/** wklx,
  deny /sys/f[^s]*/** wklx,
  deny /sys/fs/[^c]*/** wklx,
  deny /sys/fs/c[^g]*/** wklx,
  deny /sys/fs/cg[^r]*/** wklx,
  deny /sys/firmware/** rwklx,
  deny /sys/devices/virtual/powercap/** rwklx,
  deny /sys/kernel/security/** rwklx,

  # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
  ptrace (trace,read,tracedby,readby) peer=docker-default,
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment