Skip to content

Commit 3ce9498

Browse files
FabienGhdvmojzis
authored andcommitted
Add LXD support
Add support for LXD containers, enabling the generation of SELinux policies for LXD-managed containers. Signed-off-by: FabienGhd <[email protected]>
1 parent 7fa8143 commit 3ce9498

File tree

5 files changed

+272
-13
lines changed

5 files changed

+272
-13
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Udica supports following container engines:
3030
* docker v1.13+
3131
* podman v2.0+
3232
* containerd v1.5.0+ (using `nerdctl` v0.14+ or crictl)
33+
* LXD v5.21.1+
3334

3435
## Installing
3536

tests/test_basic.lxd.json

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"architecture": "x86_64",
3+
"config": {
4+
"image.architecture": "amd64",
5+
"image.description": "ubuntu 20.04 LTS amd64 (release) (20240626)",
6+
"image.label": "release",
7+
"image.os": "ubuntu",
8+
"image.release": "focal",
9+
"image.serial": "20240626",
10+
"image.type": "squashfs",
11+
"image.version": "20.04",
12+
"volatile.base_image": "afeb6fc84380878e47e1be18b3cd4e0a6671610f94ad3ffc8a50481afbe77a19",
13+
"volatile.cloud-init.instance-id": "402d6e74-3ce0-483c-8d46-fdb28cdab306",
14+
"volatile.eth0.host_name": "veth21ca03ab",
15+
"volatile.eth0.hwaddr": "00:16:3e:fc:a1:ec",
16+
"volatile.idmap.base": "0",
17+
"volatile.idmap.current": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]",
18+
"volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]",
19+
"volatile.last_state.idmap": "[]",
20+
"volatile.last_state.power": "RUNNING",
21+
"volatile.last_state.ready": "false",
22+
"volatile.uuid": "5835af62-6142-4a7c-9282-35d31ae22cb7",
23+
"volatile.uuid.generation": "5835af62-6142-4a7c-9282-35d31ae22cb7"
24+
},
25+
"created_at": "2024-06-29T16:03:00.781248448Z",
26+
"description": "",
27+
"devices": {},
28+
"ephemeral": false,
29+
"expanded_config": {
30+
"image.architecture": "amd64",
31+
"image.description": "ubuntu 20.04 LTS amd64 (release) (20240626)",
32+
"image.label": "release",
33+
"image.os": "ubuntu",
34+
"image.release": "focal",
35+
"image.serial": "20240626",
36+
"image.type": "squashfs",
37+
"image.version": "20.04",
38+
"volatile.base_image": "afeb6fc84380878e47e1be18b3cd4e0a6671610f94ad3ffc8a50481afbe77a19",
39+
"volatile.cloud-init.instance-id": "402d6e74-3ce0-483c-8d46-fdb28cdab306",
40+
"volatile.eth0.host_name": "veth21ca03ab",
41+
"volatile.eth0.hwaddr": "00:16:3e:fc:a1:ec",
42+
"volatile.idmap.base": "0",
43+
"volatile.idmap.current": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]",
44+
"volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]",
45+
"volatile.last_state.idmap": "[]",
46+
"volatile.last_state.power": "RUNNING",
47+
"volatile.last_state.ready": "false",
48+
"volatile.uuid": "5835af62-6142-4a7c-9282-35d31ae22cb7",
49+
"volatile.uuid.generation": "5835af62-6142-4a7c-9282-35d31ae22cb7"
50+
},
51+
"expanded_devices": {
52+
"eth0": {
53+
"name": "eth0",
54+
"network": "lxdbr0",
55+
"type": "nic"
56+
},
57+
"home": {
58+
"path": "/home",
59+
"readonly": "true",
60+
"source": "/home",
61+
"type": "disk"
62+
},
63+
"myport21": {
64+
"bind": "host",
65+
"connect": "tcp:127.0.0.1:21",
66+
"listen": "tcp:0.0.0.0:21",
67+
"type": "proxy"
68+
},
69+
"root": {
70+
"path": "/",
71+
"pool": "default",
72+
"type": "disk"
73+
},
74+
"spool": {
75+
"path": "/var/spool",
76+
"source": "/var/spool",
77+
"type": "disk"
78+
}
79+
},
80+
"last_used_at": "2024-07-01T15:00:22.55170503Z",
81+
"location": "none",
82+
"name": "my-ubuntu-container",
83+
"profiles": [
84+
"default",
85+
"myprofile"
86+
],
87+
"project": "default",
88+
"stateful": false,
89+
"status": "Running",
90+
"status_code": 103,
91+
"type": "container"
92+
}

udica/parse.py

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,15 @@
1616
import abc
1717
import json
1818

19-
#: Constant for the podman engine
19+
#: Constants for container engines
2020
ENGINE_PODMAN = "podman"
21-
22-
#: Constant for the cri-o engine
2321
ENGINE_CRIO = "CRI-O"
24-
25-
#: Constant for the docker engine
2622
ENGINE_DOCKER = "docker"
27-
28-
#: Constant for the containerd engine
2923
ENGINE_CONTAINERD = "containerd"
24+
ENGINE_LXD = "LXD"
3025

3126
#: All supported engines
32-
ENGINE_ALL = [ENGINE_PODMAN, ENGINE_CRIO, ENGINE_DOCKER, ENGINE_CONTAINERD]
27+
ENGINE_ALL = [ENGINE_PODMAN, ENGINE_CRIO, ENGINE_DOCKER, ENGINE_CONTAINERD, ENGINE_LXD]
3328

3429

3530
# Decorator for verifying that getting value from "data" won't
@@ -69,17 +64,34 @@ def json_is_containerd_format(json_rep):
6964

7065
def json_is_podman_format(json_rep):
7166
"""Check if the inspected file is in a format from podman."""
72-
return isinstance(json_rep, list) and (
73-
"container=oci" in json_rep[0]["Config"]["Env"]
74-
or "container=podman" in json_rep[0]["Config"]["Env"]
67+
return (
68+
isinstance(json_rep, list)
69+
and (
70+
"container=oci" in json_rep[0]["Config"]["Env"]
71+
or "container=podman" in json_rep[0]["Config"]["Env"]
72+
)
73+
)
74+
75+
def json_is_lxd_format(json_rep):
76+
"""Check if the inspected file is in a format from LXD."""
77+
return (
78+
# LXD's inspection output returns a single dictionary for a single container
79+
isinstance(json_rep, dict)
80+
and "expanded_devices" in json_rep
81+
and "architecture" in json_rep
82+
and "config" in json_rep
7583
)
7684

7785

7886
def get_engine_helper(data, ContainerEngine):
7987
engine = validate_container_engine(ContainerEngine)
88+
8089
if engine == "-":
8190
json_rep = json.loads(data)
82-
if json_is_list(json_rep):
91+
92+
if json_is_lxd_format(json_rep):
93+
return LxdHelper()
94+
elif json_is_list(json_rep):
8395
if json_is_containerd_format(json_rep):
8496
return ContainerdHelper()
8597
elif json_is_podman_format(json_rep):
@@ -96,7 +108,9 @@ def get_engine_helper(data, ContainerEngine):
96108
return CrioHelper()
97109
elif engine == ENGINE_CONTAINERD:
98110
return ContainerdHelper()
99-
raise RuntimeError("Unkown engine")
111+
elif engine == ENGINE_LXD:
112+
return LxdHelper()
113+
raise RuntimeError("Unknown engine")
100114

101115

102116
class EngineHelper(abc.ABC):
@@ -259,6 +273,49 @@ def get_caps(self, data, opts):
259273
return opts["Caps"].split(",")
260274
return data[0]["Spec"]["process"]["capabilities"]["effective"]
261275

276+
class LxdHelper(EngineHelper):
277+
def __init__(self):
278+
super().__init__(ENGINE_LXD)
279+
280+
@getter_decorator
281+
def get_devices(self, data):
282+
# Extract devices from the config
283+
devices = []
284+
config_devices = data["expanded_devices"]
285+
for name, device in config_devices.items():
286+
if device["type"] == "unix-block" or device["type"] == "unix-char":
287+
device["PathOnHost"] = device.get("path", "")
288+
devices.append(device)
289+
return devices
290+
291+
@getter_decorator
292+
def get_mounts(self, data):
293+
# Extract mounts (disk devices)
294+
mounts = []
295+
config_devices = data["expanded_devices"]
296+
for name, device in config_devices.items():
297+
if device["type"] == "disk":
298+
mounts.append(device)
299+
return mounts
300+
301+
@getter_decorator
302+
def get_ports(self, data):
303+
# Extract port information from the LXD JSON configuration
304+
ports = []
305+
for name, device in data["expanded_devices"].items():
306+
if device["type"] == "proxy":
307+
port_info = {
308+
"portNumber": int(device["listen"].split(":")[-1]),
309+
"protocol": device["connect"].split(":")[0]
310+
}
311+
ports.append(port_info)
312+
return ports
313+
314+
@getter_decorator
315+
def get_caps(self, data, opts):
316+
# Capabilities are not part of LXD spec directly
317+
return []
318+
262319

263320
def parse_cap(data):
264321
return data.decode().split("\n")[1].split(",")

udica/perms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# You should have received a copy of the GNU General Public License
1414
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1515

16+
# Dictionary of permissions for various device and file types
1617
perm = {
1718
"device_rw": "getattr read write append ioctl lock open",
1819
"dir_rw": "add_name create getattr ioctl lock open read remove_name rmdir search setattr write",
@@ -25,4 +26,5 @@
2526
"socket_ro": "getattr open read",
2627
}
2728

29+
# Dictionary of socket types mapped to their corresponding SELinux object class
2830
socket = {"tcp": "tcp_socket", "udp": "udp_socket", "sctp": "sctp_socket"}

udica/policy.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ def create_policy(
181181
write_policy_for_crio_mounts(mounts, policy)
182182
elif inspect_format == "containerd":
183183
write_policy_for_containerd_mounts(mounts, policy)
184+
elif inspect_format == "LXD":
185+
write_policy_for_lxd_mounts(mounts, policy)
184186
else:
185187
write_policy_for_podman_mounts(mounts, policy)
186188

@@ -567,6 +569,111 @@ def write_policy_for_containerd_mounts(mounts, policy):
567569
)
568570

569571

572+
def write_policy_for_lxd_mounts(mounts, policy):
573+
for item in sorted(mounts, key=lambda x: str(x["source"])):
574+
if not item["source"].find("/"):
575+
if item["source"] == LOG_CONTAINER and "ro" in item.get("options", []):
576+
policy.write(" (blockinherit log_container)\n")
577+
add_template("log_container")
578+
continue
579+
580+
if item["source"] == LOG_CONTAINER and "ro" not in item.get("options", []):
581+
policy.write(" (blockinherit log_rw_container)\n")
582+
add_template("log_container")
583+
continue
584+
585+
if item["source"] == HOME_CONTAINER and "ro" in item.get("options", []):
586+
policy.write(" (blockinherit home_container)\n")
587+
add_template("home_container")
588+
continue
589+
590+
if item["source"] == HOME_CONTAINER and "ro" not in item.get("options", []):
591+
policy.write(" (blockinherit home_rw_container)\n")
592+
add_template("home_container")
593+
continue
594+
595+
if item["source"] == TMP_CONTAINER and "ro" in item.get("options", []):
596+
policy.write(" (blockinherit tmp_container)\n")
597+
add_template("tmp_container")
598+
continue
599+
600+
if item["source"] == TMP_CONTAINER and "ro" not in item.get("options", []):
601+
policy.write(" (blockinherit tmp_rw_container)\n")
602+
add_template("tmp_container")
603+
continue
604+
605+
if item["source"] == CONFIG_CONTAINER and "ro" in item.get("options", []):
606+
policy.write(" (blockinherit config_container)\n")
607+
add_template("config_container")
608+
continue
609+
610+
if item["source"] == CONFIG_CONTAINER and "ro" not in item.get("options", []):
611+
policy.write(" (blockinherit config_rw_container)\n")
612+
add_template("config_container")
613+
continue
614+
615+
contexts = list_contexts(item["source"])
616+
for context in contexts:
617+
if "ro" not in item.get("options", []):
618+
policy.write(
619+
" (allow process "
620+
+ context
621+
+ " ( dir ( "
622+
+ perms.perm["dir_rw"]
623+
+ " ))) \n"
624+
)
625+
policy.write(
626+
" (allow process "
627+
+ context
628+
+ " ( file ( "
629+
+ perms.perm["file_rw"]
630+
+ " ))) \n"
631+
)
632+
policy.write(
633+
" (allow process "
634+
+ context
635+
+ " ( fifo_file ( "
636+
+ perms.perm["fifo_rw"]
637+
+ " ))) \n"
638+
)
639+
policy.write(
640+
" (allow process "
641+
+ context
642+
+ " ( sock_file ( "
643+
+ perms.perm["socket_rw"]
644+
+ " ))) \n"
645+
)
646+
if "ro" in item.get("options", []):
647+
policy.write(
648+
" (allow process "
649+
+ context
650+
+ " ( dir ( "
651+
+ perms.perm["dir_ro"]
652+
+ " ))) \n"
653+
)
654+
policy.write(
655+
" (allow process "
656+
+ context
657+
+ " ( file ( "
658+
+ perms.perm["file_ro"]
659+
+ " ))) \n"
660+
)
661+
policy.write(
662+
" (allow process "
663+
+ context
664+
+ " ( fifo_file ( "
665+
+ perms.perm["fifo_ro"]
666+
+ " ))) \n"
667+
)
668+
policy.write(
669+
" (allow process "
670+
+ context
671+
+ " ( sock_file ( "
672+
+ perms.perm["socket_ro"]
673+
+ " ))) \n"
674+
)
675+
676+
570677
def load_policy(opts):
571678
PWD = getcwd()
572679

0 commit comments

Comments
 (0)