1
1
Fork 0
mirror of https://github.com/containers/udica synced 2024-05-10 23:36:11 +02:00

Add option to generate custom policy for a confined user

Udica can now generate cil policy for a confined user using a list of
macros.
The macros are based on policy templates created by Patrik Končitý:
https://github.com/Koncpa/confined-users-policy

Signed-off-by: Vit Mojzis <vmojzis@redhat.com>
This commit is contained in:
Vit Mojzis 2023-11-29 10:38:48 +01:00
parent 106a80f399
commit 3cda61f9a5
5 changed files with 4779 additions and 115 deletions

View File

@ -170,6 +170,56 @@ SELinux now allows binding to tcp/udp port *21*, but not to *80*:
Ncat: SHA-1 fingerprint: 6EEC 102E 6666 5F96 CC4F E5FA A1BE 4A5E 6C76 B6DC
Ncat: bind to :::80: Permission denied. QUITTING.
## Creating SELinux policy for confined user
Each Linux user on an SELinux-enabled system is mapped to an SELinux user. By default administrators can choose between the following SELinux users when confining a user account: root, staff_u, sysadm_u, user_u, xguest_u, guest_u (and unconfined_u which does not limit the user's actions).
To give administrators more options in confining users, *udica* now provides a way to generate a custom SELinux user (and corresponding roles and types) based on the specified parameters. The new user policy is assembled using a set of predefined policy macros based on use-cases (managing network, administrative tasks, etc.).
To generate a confined user, use the "confined_user" keyword followed by a list of options:
| Option | Use case |
| ------------- | ------------- |
| -a, --admin_commands | Use administrative commands (vipw, passwd, ...) |
| -g, --graphical_login | Use graphical login environment |
| -m, --mozilla_usage | Use mozilla firefox |
| -n, --networking | Manage basic networking (ip, ifconfig, traceroute, tcpdump, ...) |
| -d, --security_advanced | Manage SELinux settings (semanage, semodule, sepolicy, ...) |
| -i, --security_basic | Use read-only security-related tools (seinfo, getsebool, sesearch, ...) |
| -s, --sudo | Run commands as root using sudo |
| -l, --user_login | Basic rules common to all users (tty, pty, ...) |
| -c, --ssh_connect | Connect over SSH |
| -b, --basic_commands | Use basic commands (date, ls, ps, man, systemctl -user, journalctl -user, passwd, ...) |
The new user also needs to be assigned an MLS/MCS level and range. These are set to `s0` and `s0:c0.c1023` respectively by default to work well in *targeted* policy mode.
For more details see [Red Hat Multi-Level Security documentation](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/using_selinux/index#using-multi-level-security-mls_using-selinux).
```
$ udica confined_user -abcdgilmns --level s0 --range "s0:c0" custom_user
Created custom_user.cil
Run the following commands to apply the new policy:
Install the new policy module
# semodule -i custom_user.cil /usr/share/udica/macros/confined_user_macros.cil
Create a default context file for the new user
# sed -e s|user|custom_user|g /etc/selinux/targeted/contexts/users/user_u > /etc/selinux/targeted/contexts/users/custom_user_u
Map the new selinux user to an existing user account
# semanage login -a -s custom_user_u custom_user
Fix labels in the user's home directory
# restorecon -RvF /home/custom_user
```
As prompted by *udica*, the new user policy needs to be installed into the system along with the *confined_user_macros* file and a *default context* file needs to be created before the policy is ready to be used.
Last step is either assignment to an existing linux user (using `semanage login`), or specifying the new SELinux user when creating a new linux user account (no need to run `restorecon` for a new user home directory).
```
useradd -Z custom_user_u
```
The created policy defines a new SELinux user `<user_name>_u`, a corresponding role `<user_name>_r` and a list of types (varies based on selected options) `<user_name>_t, <user_name>_sudo_t, <user_name>_ssh_agent_t, ...`
See [Red Hat Confined User documentation](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/using_selinux/managing-confined-and-unconfined-users_using-selinux#doc-wrapper) for more details about confined users, their assignment, available roles and access they allow.
## SELinux labels vs. objects they represent
Policies generated by *udica* work with **SELinux labels** as opposed to filesystem paths, port numbers etc. This means that allowing access to given path (e.g. path to a directory mounted to your container), port number, or any other resource may also allow access to other resources you didn't specify, since the same SELinux label can be assigned to multiple resources.

View File

@ -37,6 +37,7 @@ setuptools.setup(
data_files=[
("/usr/share/licenses/udica", ["LICENSE"]),
("/usr/share/udica/ansible", ["udica/ansible/deploy-module.yml"]),
("/usr/share/udica/macros", ["udica/macros/confined_user_macros.cil"]),
],
# scripts=["bin/udica"],
entry_points={"console_scripts": ["udica=udica.__main__:main"]},

View File

@ -13,8 +13,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import subprocess
import argparse
import subprocess
import sys
# import udica
from udica.parse import parse_avc_file
@ -25,116 +26,224 @@ from udica.policy import create_policy, load_policy, generate_playbook
def get_args():
parser = argparse.ArgumentParser(
description="Script generates SELinux policy for running container."
)
parser.add_argument("-V", "--version", action="version", version=version)
parser.add_argument(
type=str, help="Name for SELinux policy module", dest="ContainerName"
)
parser.add_argument(
"-i",
"--container-id",
type=str,
help="Running container ID",
dest="ContainerID",
default=None,
)
parser.add_argument(
"-j",
"--json",
help='Load json from this file, use "-j -" for stdin',
required=False,
dest="JsonFile",
default=None,
)
parser.add_argument(
"--full-network-access",
help="Allow container full Network access ",
required=False,
dest="FullNetworkAccess",
action="store_true",
)
parser.add_argument(
"--tty-access",
help="Allow container to read and write the controlling terminal ",
required=False,
dest="TtyAccess",
action="store_true",
)
parser.add_argument(
"--X-access",
help="Allow container to communicate with Xserver ",
required=False,
dest="XAccess",
action="store_true",
)
parser.add_argument(
"--virt-access",
help="Allow container to communicate with libvirt ",
required=False,
dest="VirtAccess",
action="store_true",
)
parser.add_argument(
"-s",
"--stream-connect",
help="Allow container to stream connect with given SELinux domain ",
required=False,
dest="StreamConnect",
)
parser.add_argument(
"-l",
"--load-modules",
help="Load templates and module created by this tool ",
required=False,
dest="LoadModules",
action="store_true",
)
parser.add_argument(
"-c",
"--caps",
help='List of capabilities, e.g "-c AUDIT_WRITE,CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,MKNOD,NET_BIND_SERVICE,NET_RAW,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT"',
required=False,
dest="Caps",
default=None,
)
parser.add_argument(
"--devices",
type=str,
help='List of devices the container should have access to, e.g "--devices /dev/dri/card0,/dev/dri/renderD128"',
dest="Devices",
required=False,
default=None,
)
parser.add_argument(
"-d",
"--ansible",
help="Generate ansible playbook to deploy SELinux policy for containers ",
required=False,
dest="Ansible",
action="store_true",
)
parser.add_argument(
"-a",
"--append-rules",
type=str,
help="Append more SELinux allow rules from file",
dest="FileAVCS",
required=False,
default=None,
)
parser.add_argument(
"-e",
"--container-engine",
type=str,
help="Specify which container engine is used for the inspected container (supports: {})".format(
", ".join(ENGINE_ALL)
),
dest="ContainerEngine",
required=False,
default="-",
)
if "confined_user" in sys.argv:
# set up confined_user parser (do not show normal "udica" options)
parser = argparse.ArgumentParser(
description="SELinux confined user policy generator"
)
parser.add_argument("confined_user")
parser.add_argument(
"-a",
"--admin_commands",
action="store_true",
default=False,
dest="admin_commands",
help="Use administrative commands (vipw, passwd, ...)",
)
parser.add_argument(
"-g",
"--graphical_login",
action="store_true",
default=False,
dest="graphical_login",
help="Use graphical login environment",
)
parser.add_argument(
"-m",
"--mozilla_usage",
action="store_true",
default=False,
dest="mozilla_usage",
help="Use mozilla firefox",
)
parser.add_argument(
"-n",
"--networking",
action="store_true",
default=False,
dest="networking",
help="Manage basic networking (ip, ifconfig, traceroute, tcpdump, ...)",
)
parser.add_argument(
"-d",
"--security_advanced",
action="store_true",
default=False,
dest="security_advanced",
help="Manage SELinux settings (semanage, semodule, sepolicy, ...)",
)
parser.add_argument(
"-i",
"--security_basic",
action="store_true",
default=False,
dest="security_basic",
help="Use read-only security-related tools (seinfo, getsebool, sesearch, ...)",
)
parser.add_argument(
"-s",
"--sudo",
action="store_true",
default=False,
dest="sudo",
help="Run commands as root using sudo",
)
parser.add_argument(
"-l",
"--user_login",
action="store_true",
default=False,
dest="user_login",
help="Basic rules common to all users (tty, pty, ...)",
)
parser.add_argument(
"-c",
"--ssh_connect",
action="store_true",
default=False,
dest="ssh_connect",
help="Connect over SSH",
)
parser.add_argument(
"-b",
"--basic_commands",
action="store_true",
default=False,
dest="basic_commands",
help="Use basic commands (date, ls, ps, man, systemctl -user, journalctl -user, passwd, ...)",
)
parser.add_argument(
"--level",
nargs="?",
default="s0",
dest="level",
help='MLS/MCS level, defaults to "s0"',
)
parser.add_argument(
"--range",
nargs="?",
default="s0-s0:c0.c1023",
dest="range",
help='MLS/MCS range, defaults to "s0-s0:c0.c1023"',
)
parser.add_argument("uname")
else:
# set up normal udica parser
parser = argparse.ArgumentParser(
description="Script generates SELinux policy for running container.",
prog="udica [confined_user]",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Additional options:
confined_user Generate policy for a new confined user instead of a container policy""",
)
parser.add_argument("-V", "--version", action="version", version=version)
parser.add_argument(
type=str, help="Name for SELinux policy module", dest="ContainerName"
)
parser.add_argument(
"-i",
"--container-id",
type=str,
help="Running container ID",
dest="ContainerID",
default=None,
)
parser.add_argument(
"-j",
"--json",
help='Load json from this file, use "-j -" for stdin',
required=False,
dest="JsonFile",
default=None,
)
parser.add_argument(
"--full-network-access",
help="Allow container full Network access ",
required=False,
dest="FullNetworkAccess",
action="store_true",
)
parser.add_argument(
"--tty-access",
help="Allow container to read and write the controlling terminal ",
required=False,
dest="TtyAccess",
action="store_true",
)
parser.add_argument(
"--X-access",
help="Allow container to communicate with Xserver ",
required=False,
dest="XAccess",
action="store_true",
)
parser.add_argument(
"--virt-access",
help="Allow container to communicate with libvirt ",
required=False,
dest="VirtAccess",
action="store_true",
)
parser.add_argument(
"-s",
"--stream-connect",
help="Allow container to stream connect with given SELinux domain ",
required=False,
dest="StreamConnect",
)
parser.add_argument(
"-l",
"--load-modules",
help="Load templates and module created by this tool ",
required=False,
dest="LoadModules",
action="store_true",
)
parser.add_argument(
"-c",
"--caps",
help='List of capabilities, e.g "-c AUDIT_WRITE,CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,MKNOD,NET_BIND_SERVICE,NET_RAW,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT"',
required=False,
dest="Caps",
default=None,
)
parser.add_argument(
"--devices",
type=str,
help='List of devices the container should have access to, e.g "--devices /dev/dri/card0,/dev/dri/renderD128"',
dest="Devices",
required=False,
default=None,
)
parser.add_argument(
"-d",
"--ansible",
help="Generate ansible playbook to deploy SELinux policy for containers ",
required=False,
dest="Ansible",
action="store_true",
)
parser.add_argument(
"-a",
"--append-rules",
type=str,
help="Append more SELinux allow rules from file",
dest="FileAVCS",
required=False,
default=None,
)
parser.add_argument(
"-e",
"--container-engine",
type=str,
help="Specify which container engine is used for the inspected container (supports: {})".format(
", ".join(ENGINE_ALL)
),
dest="ContainerEngine",
required=False,
default="-",
)
args = parser.parse_args()
return vars(args)
@ -142,6 +251,13 @@ def get_args():
def main():
opts = get_args()
# generate confined user policy
if "confined_user" in opts.keys():
from udica.confined_user import create_confined_user_policy
create_confined_user_policy(opts)
return
if opts["ContainerID"]:
container_inspect_raw = None
for backend in [ENGINE_PODMAN, ENGINE_DOCKER]:
@ -167,8 +283,6 @@ def main():
if opts["JsonFile"]:
if opts["JsonFile"] == "-":
import sys
container_inspect_raw = sys.stdin.read()
else:
import os.path
@ -182,8 +296,6 @@ def main():
if (not opts["JsonFile"]) and (not opts["ContainerID"]):
try:
import sys
container_inspect_raw = sys.stdin.read()
except Exception as e:
print("Couldn't parse inspect data from stdin:", e)

134
udica/confined_user.py Normal file
View File

@ -0,0 +1,134 @@
# Copyright (C) 2023 Vit Mojzis, <vmojzis@redhat.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
MACRO_CALLS = {
"admin_commands": (
"(call confinedom_admin_commands_macro ({}))",
("_t", "_r", "_sudo_t"),
),
"graphical_login": (
"(call confinedom_graphical_login_macro ({}))",
("_t", "_r", "_dbus_t"),
),
"mozilla_usage": ("(call confinedom_mozilla_usage_macro ({}))", ("_t", "_r")),
"networking": ("(call confinedom_networking_macro ({}))", ("_t", "_r")),
"security_advanced": (
"(call confinedom_security_advanced_macro ({}))",
("_t", "_r", "_sudo_t", "_userhelper_t"),
),
"security_basic": ("(call confinedom_security_basic_macro ({}))", ("_t", "_r")),
"sudo": (
"(call confinedom_sudo_macro ({}))",
("_t", "_r", "_sudo_t", "_sudo_tmp_t"),
),
"user_login": (
"(call confinedom_user_login_macro ({}))",
("_t", "_r", "_gkeyringd_t", "_dbus_t", "_exec_content"),
),
"ssh_connect": (
"(call confined_ssh_connect_macro ({}))",
("_t", "_r", "_ssh_agent_t"),
),
"basic_commands": ("(call confined_use_basic_commands_macro ({}))", ("_t", "_r")),
}
TYPE_DEFS = {
"_t": "(type {}_t)",
"_r": "(role {}_r)",
"_dbus_t": "(type {}_dbus_t)",
"_gkeyringd_t": "(type {}_gkeyringd_t)",
"_ssh_agent_t": "(type {}_ssh_agent_t)",
"_sudo_t": "(type {}_sudo_t)",
"_sudo_tmp_t": "(type {}_sudo_tmp_t)",
"_userhelper_t": "(type {}_userhelper_t)",
"_exec_content": "(boolean {}_exec_content true)",
}
def create_confined_user_policy(opts):
# MCS/MLS range handling - needs to be separated into up-to 4 parts
# s0-s15:c0.c1023 -> (userrange {uname}_u ((s0 ) (s15 (range c0 c1023))))
# s0:c0 -> (userrange {uname}_u ((s0 ) (s0 (c0))))
mls_range = opts["range"]
mcs_range = ""
# separate MCS portion
if ":" in opts["range"]:
# s0:c0.c1023
(mls_range, mcs_range) = opts["range"].split(":")
if "-" in mls_range:
# s0-s15
(range_l, range_h) = mls_range.split("-")
else:
# s0
range_l = mls_range
range_h = range_l
if mcs_range != "":
if "." in mcs_range:
# s0:c0.c1023 -> (userrange {uname}_u ((s0 ) (s0 (range c0 c1023))))
(mcs_range_l, mcs_range_h) = mcs_range.split(".")
mcs_range = "(range {} {})".format(mcs_range_l, mcs_range_h)
else:
# s0:c0 -> (userrange {uname}_u ((s0 ) (s0 (c0))))
mcs_range = "({})".format(mcs_range)
range = "({} ) ({} {})".format(range_l, range_h, mcs_range)
defs = set()
policy = """
(user {uname}_u)
(userrole {uname}_u {uname}_r)
(userlevel {uname}_u ({level}))
(userrange {uname}_u ({range}))
""".format(
uname=opts["uname"], level=opts["level"], range=range
)
# process arguments determining which macros are to be used
for arg, value in opts.items():
if not value or arg not in MACRO_CALLS.keys():
continue
for param in MACRO_CALLS[arg][1]:
defs.add(TYPE_DEFS[param].format(opts["uname"]))
policy += "\n" + (
MACRO_CALLS[arg][0].format(
" ".join([opts["uname"] + s for s in MACRO_CALLS[arg][1]])
)
)
# print("{}: {}".format(arg, value))
policy = "\n".join(sorted(defs)) + policy
with open("{}.cil".format(opts["uname"]), "w") as f:
f.write(policy)
print("Created {}.cil".format(opts["uname"]))
print("Run the following commands to apply the new policy:")
print("Install the new policy module")
print(
"# semodule -i {}.cil /usr/share/udica/macros/confined_user_macros.cil".format(
opts["uname"]
)
)
print("Create a default context file for the new user")
print(
"# sed -e s|user|{}|g /etc/selinux/targeted/contexts/users/user_u > /etc/selinux/targeted/contexts/users/{}_u".format(
opts["uname"], opts["uname"]
)
)
print("Map the new selinux user to an existing user account")
print("# semanage login -a -s {}_u {}".format(opts["uname"], opts["uname"]))
print("Fix labels in the user's home directory")
print("# restorecon -RvF /home/{}".format(opts["uname"]))

File diff suppressed because it is too large Load Diff