underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
186 lines (172 loc) • 8 kB
JavaScript
/**
* SELinux policy, labeling, and enforcement command helpers.
*
* @module src/server/selinux.js
* @namespace SELinuxService
*/
;
/**
* Main SELinux utility.
* @class SELinuxService
* @memberof SELinuxService
*/
class SELinuxService {
/** Shared container label every unprivileged container domain can read and write. */
static SHARED_CONTAINER_TYPE = 'container_file_t';
/**
* Quotes one shell argument used by generated SELinux commands.
* @param {*} value - Value to quote.
* @returns {string}
*/
static shellArgumentFactory(value) {
return `'${`${value ?? ''}`.replaceAll("'", `'"'"'`)}'`;
}
/**
* Builds the Rocky/RHEL SELinux userspace installation command.
* @param {{sudo?: boolean}} [options]
* @returns {string}
*/
static selinuxPackagesCommandFactory({ sudo = true } = {}) {
return `${sudo ? 'sudo ' : ''}dnf install -y policycoreutils policycoreutils-python-utils selinux-policy-targeted audit`;
}
/**
* Builds commands that make Enforcing mode persistent and active.
* @param {{sudo?: boolean, restorePaths?: string[]}} [options]
* @returns {string[]}
*/
static selinuxEnforcingCommandsFactory({ sudo = true, restorePaths = [] } = {}) {
const prefix = sudo ? 'sudo ' : '';
return [
`if [ -f /etc/selinux/config ]; then ${prefix}sed -i -E 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config; fi`,
// A host running with SELinux Disabled has an unlabeled filesystem, so the
// config flip alone would boot it into Enforcing with nothing labeled.
// `setenforce` cannot activate the mode from Disabled either: the switch
// completes on the next boot, and only after this relabel pass.
`if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Disabled" ]; then ${prefix}touch /.autorelabel; fi`,
...(restorePaths.length > 0
? [SELinuxService.selinuxRestoreconCommandFactory(restorePaths, { sudo })]
: []),
`if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then ${prefix}setenforce 1; fi`,
];
}
/**
* Builds a command that restores policy-defined file contexts.
* @param {string|string[]} paths - Files or directories to label.
* @param {{recursive?: boolean, sudo?: boolean}} [options]
* @returns {string}
*/
static selinuxRestoreconCommandFactory(paths, { recursive = true, sudo = true } = {}) {
const values = (Array.isArray(paths) ? paths : [paths]).filter(Boolean);
if (values.length === 0) throw new TypeError('selinuxRestoreconCommandFactory requires at least one path');
const operations = values
.map(SELinuxService.shellArgumentFactory)
.map(
(path) =>
`{ [ ! -e ${path} ] || ${sudo ? 'sudo ' : ''}restorecon ${recursive ? '-RF ' : ''}${path}; }`,
)
.join(' && ');
return `if command -v restorecon >/dev/null 2>&1; then ${operations}; fi`;
}
/**
* Builds an idempotent persistent file context mapping.
* @param {string} path - Directory or file prefix to map.
* @param {{type: string, sudo?: boolean}} options
* @returns {string}
*/
static selinuxFileContextCommandFactory(path, { type, sudo = true } = {}) {
if (!path) throw new TypeError('selinuxFileContextCommandFactory requires a path');
if (!type) throw new TypeError('selinuxFileContextCommandFactory requires a type');
const prefix = sudo ? 'sudo ' : '';
const expression = SELinuxService.shellArgumentFactory(`${path}(/.*)?`);
return `if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then command -v semanage >/dev/null 2>&1 || { echo 'semanage is required for persistent file contexts' >&2; exit 1; }; ${prefix}semanage fcontext -a -t ${type} ${expression} 2>/dev/null || ${prefix}semanage fcontext -m -t ${type} ${expression}; fi`;
}
/**
* Builds persistent labeling commands for host paths bind-mounted into
* unprivileged containers. `container_t` cannot read the policy defaults of
* those trees (`kubernetes_file_t`, `var_lib_t`), and the mapping is
* registered before the files exist so entries created later inherit the
* shared label instead of requiring another relabel pass.
* @param {string|string[]} paths - Files or directories to share.
* @param {{sudo?: boolean}} [options]
* @returns {string[]}
*/
static selinuxContainerSharedContextCommandsFactory(paths, { sudo = true } = {}) {
const values = (Array.isArray(paths) ? paths : [paths]).filter(Boolean);
if (values.length === 0)
throw new TypeError('selinuxContainerSharedContextCommandsFactory requires at least one path');
return [
...values.map((path) =>
SELinuxService.selinuxFileContextCommandFactory(path, { type: SELinuxService.SHARED_CONTAINER_TYPE, sudo }),
),
SELinuxService.selinuxRestoreconCommandFactory(values, { sudo }),
];
}
/**
* Builds persistent labeling commands for an SSH directory.
* Standard /root and /home locations already have policy mappings; custom
* home locations receive an explicit ssh_home_t mapping.
* @param {{sshDirectory: string, sudo?: boolean}} options
* @returns {string[]}
*/
static selinuxSshContextCommandsFactory({ sshDirectory, sudo = true } = {}) {
if (!sshDirectory) throw new TypeError('selinuxSshContextCommandsFactory requires sshDirectory');
const prefix = sudo ? 'sudo ' : '';
const standard = sshDirectory === '/root/.ssh' || /^\/home\/[^/]+\/\.ssh$/.test(sshDirectory);
const commands = [];
if (!standard) {
const expression = SELinuxService.shellArgumentFactory(`${sshDirectory}(/.*)?`);
commands.push(
`if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then command -v semanage >/dev/null 2>&1 || { echo 'semanage is required for a custom SSH home' >&2; exit 1; }; ${prefix}semanage fcontext -a -t ssh_home_t ${expression} 2>/dev/null || ${prefix}semanage fcontext -m -t ssh_home_t ${expression}; fi`,
);
}
commands.push(SELinuxService.selinuxRestoreconCommandFactory(sshDirectory, { sudo }));
return commands;
}
/**
* Builds an idempotent ssh_port_t assignment for a custom SSH port.
* @param {{port?: number|string, sudo?: boolean}} [options]
* @returns {string[]}
*/
static selinuxSshPortCommandsFactory({ port = 22, sudo = true } = {}) {
const value = Number(port);
if (!Number.isInteger(value) || value < 1 || value > 65535) throw new RangeError('SSH port must be 1-65535');
if (value === 22) return [];
const prefix = sudo ? 'sudo ' : '';
return [
`if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then command -v semanage >/dev/null 2>&1 || { echo 'semanage is required for a custom SSH port' >&2; exit 1; }; ${prefix}semanage port -a -t ssh_port_t -p tcp ${value} 2>/dev/null || ${prefix}semanage port -m -t ssh_port_t -p tcp ${value}; fi`,
];
}
/**
* Executes a generated command list.
* @param {string[]} [commands]
* @param {{execute: Function}} options
* @returns {*[]}
*/
static runSELinuxCommands(commands = [], { execute } = {}) {
if (typeof execute !== 'function') throw new TypeError('runSELinuxCommands requires an executor');
return commands.map((command) => execute(command));
}
}
const {
runSELinuxCommands,
selinuxContainerSharedContextCommandsFactory,
selinuxEnforcingCommandsFactory,
selinuxFileContextCommandFactory,
selinuxPackagesCommandFactory,
selinuxRestoreconCommandFactory,
selinuxSshContextCommandsFactory,
selinuxSshPortCommandsFactory,
shellArgumentFactory,
} = SELinuxService;
export default SELinuxService;
export {
runSELinuxCommands,
selinuxContainerSharedContextCommandsFactory,
selinuxEnforcingCommandsFactory,
selinuxFileContextCommandFactory,
selinuxPackagesCommandFactory,
selinuxRestoreconCommandFactory,
selinuxSshContextCommandsFactory,
selinuxSshPortCommandsFactory,
shellArgumentFactory,
};