| |
@@ -0,0 +1,345 @@
|
| |
+ From 22c297b322ddc46eb9cd1f96cb598929e8de607e Mon Sep 17 00:00:00 2001
|
| |
+ From: Juan Antonio Osorio Robles <jaosorior@redhat.com>
|
| |
+ Date: Fri, 30 Aug 2019 17:50:01 +0300
|
| |
+ Subject: [PATCH] Initial CRI-O support
|
| |
+
|
| |
+ This takes modifies udica to also take the "inspect" format that crictl
|
| |
+ gives out, and not only the docker/podman one.
|
| |
+
|
| |
+ Note that in this implementation, only the json file works; support for
|
| |
+ parsing the input from the crictl command will come in a separate PR.
|
| |
+ ---
|
| |
+ udica/__main__.py | 43 +++++++++-------------
|
| |
+ udica/parse.py | 92 ++++++++++++++++++++++++++++++++++++-----------
|
| |
+ udica/policy.py | 80 +++++++++++++++++++++++++++++++++++------
|
| |
+ 3 files changed, 157 insertions(+), 58 deletions(-)
|
| |
+
|
| |
+ diff --git a/udica/__main__.py b/udica/__main__.py
|
| |
+ index f40c0c3..b7076bd 100644
|
| |
+ --- a/udica/__main__.py
|
| |
+ +++ b/udica/__main__.py
|
| |
+ @@ -18,7 +18,8 @@
|
| |
+ import shutil
|
| |
+
|
| |
+ # import udica
|
| |
+ -from udica.parse import parse_inspect, parse_cap, parse_is_podman, parse_avc_file
|
| |
+ +from udica.parse import parse_inspect, parse_cap, json_is_podman_format, parse_avc_file
|
| |
+ +from udica import parse
|
| |
+ from udica.policy import create_policy, load_policy, generate_playbook
|
| |
+
|
| |
+ def get_args():
|
| |
+ @@ -55,7 +56,7 @@ def main():
|
| |
+ opts = get_args()
|
| |
+
|
| |
+ if opts['ContainerID']:
|
| |
+ - container_inspect_data = None
|
| |
+ + container_inspect_raw = None
|
| |
+ for backend in ["podman", "docker"]:
|
| |
+ try:
|
| |
+ run_inspect = subprocess.Popen([backend, "inspect", opts['ContainerID']], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
| |
+ @@ -66,22 +67,22 @@ def main():
|
| |
+ inspect_data = None
|
| |
+
|
| |
+ if inspect_data:
|
| |
+ - container_inspect_data = inspect_data
|
| |
+ + container_inspect_raw = inspect_data
|
| |
+ break
|
| |
+
|
| |
+ - if not container_inspect_data:
|
| |
+ + if not container_inspect_raw:
|
| |
+ print('Container with specified ID does not exits!')
|
| |
+ exit(3)
|
| |
+
|
| |
+ if opts['JsonFile']:
|
| |
+ if opts['JsonFile'] == '-':
|
| |
+ import sys
|
| |
+ - container_inspect_data = sys.stdin.read()
|
| |
+ + container_inspect_raw = sys.stdin.read()
|
| |
+ else:
|
| |
+ import os.path
|
| |
+ if os.path.isfile(opts['JsonFile']):
|
| |
+ with open(opts['JsonFile'], 'r') as f:
|
| |
+ - container_inspect_data = f.read()
|
| |
+ + container_inspect_raw = f.read()
|
| |
+ else:
|
| |
+ print('Json file does not exists!')
|
| |
+ exit(3)
|
| |
+ @@ -89,24 +90,20 @@ def main():
|
| |
+ if (not opts['JsonFile']) and (not opts['ContainerID']):
|
| |
+ try:
|
| |
+ import sys
|
| |
+ - container_inspect_data = sys.stdin.read()
|
| |
+ + container_inspect_raw = sys.stdin.read()
|
| |
+ except Exception as e:
|
| |
+ print('Couldn\'t parse inspect data from stdin:', e)
|
| |
+ exit(3)
|
| |
+
|
| |
+ - try:
|
| |
+ - container_inspect = parse_inspect(container_inspect_data)
|
| |
+ - except Exception as e:
|
| |
+ - print('Couldn\'t parse inspect data:', e)
|
| |
+ - exit(3)
|
| |
+ - container_mounts = container_inspect[0]['Mounts']
|
| |
+ - container_ports = container_inspect[0]['NetworkSettings']['Ports']
|
| |
+
|
| |
+ try:
|
| |
+ - is_podman = parse_is_podman(container_inspect_data)
|
| |
+ + inspect_format = parse.get_inspect_format(container_inspect_raw)
|
| |
+ except Exception as e:
|
| |
+ - print('Couldn\'t parse podman:', e)
|
| |
+ + print('Couldn\'t parse inspect data:', e)
|
| |
+ exit(3)
|
| |
+ + container_inspect = parse_inspect(container_inspect_raw)
|
| |
+ + container_mounts = parse.get_mounts(container_inspect, inspect_format)
|
| |
+ + container_ports = parse.get_ports(container_inspect, inspect_format)
|
| |
+
|
| |
+ # Append allow rules if AVCs log is provided
|
| |
+ append_rules = None
|
| |
+ @@ -117,7 +114,7 @@ def main():
|
| |
+ try:
|
| |
+ append_rules = parse_avc_file(f.read())
|
| |
+ except Exception as e:
|
| |
+ - print('Couldn\'t parse inspect data:', e)
|
| |
+ + print('Couldn\'t parse AVC file:', e)
|
| |
+ exit(3)
|
| |
+ f.close()
|
| |
+ else:
|
| |
+ @@ -126,17 +123,11 @@ def main():
|
| |
+
|
| |
+ container_caps = []
|
| |
+
|
| |
+ - if opts['Caps']:
|
| |
+ - if opts['Caps'] == 'None':
|
| |
+ - container_caps = []
|
| |
+ - else:
|
| |
+ - container_caps = opts['Caps'].split(',')
|
| |
+ - else:
|
| |
+ - if is_podman:
|
| |
+ - container_caps = container_inspect[0]['EffectiveCaps']
|
| |
+ + container_caps = parse.get_caps(container_inspect, opts, inspect_format)
|
| |
+
|
| |
+ try:
|
| |
+ - create_policy(opts, container_caps, container_mounts, container_ports, append_rules)
|
| |
+ + create_policy(opts, container_caps, container_mounts, container_ports,
|
| |
+ + append_rules, inspect_format)
|
| |
+ except Exception as e:
|
| |
+ print('Couldn\'t create policy:', e)
|
| |
+ exit(4)
|
| |
+ diff --git a/udica/parse.py b/udica/parse.py
|
| |
+ index 6ed9148..d10ff1c 100644
|
| |
+ --- a/udica/parse.py
|
| |
+ +++ b/udica/parse.py
|
| |
+ @@ -15,37 +15,87 @@
|
| |
+
|
| |
+ import json
|
| |
+
|
| |
+ -def parse_inspect(data):
|
| |
+ - json_rep = json.loads(data)
|
| |
+ - if 'container=podman' not in json_rep[0]['Config']['Env']:
|
| |
+ - for item in json_rep[0]['Mounts']:
|
| |
+ - item['source'] = item['Source']
|
| |
+ - if item['Mode'] == 'rw':
|
| |
+ - item['options'] = 'rw'
|
| |
+ - if item['Mode'] == 'ro':
|
| |
+ - item['options'] = 'ro'
|
| |
+ +def json_is_podman_or_docker_format(json_rep):
|
| |
+ + """Check if the inspected file is in a format from docker or podman.
|
| |
+ +
|
| |
+ + We know this because the type in the inspect command will be a list
|
| |
+ + """
|
| |
+ + return isinstance(json_rep, list)
|
| |
+ +
|
| |
+
|
| |
+ - temp_ports = []
|
| |
+ +def json_is_podman_format(json_rep):
|
| |
+ + """Check if the inspected file is in a format from podman. """
|
| |
+ + return isinstance(json_rep, list) and 'container=podman' in json_rep[0]['Config']['Env']
|
| |
+
|
| |
+ - for item in json_rep[0]['NetworkSettings']['Ports']:
|
| |
+ - container_port = item.split('/')
|
| |
+ - host_port = json_rep[0]['NetworkSettings']['Ports'][item][0]['HostPort']
|
| |
+ - new_port = {'hostPort':int(host_port), 'protocol':container_port[1]}
|
| |
+ - temp_ports.append(new_port)
|
| |
+ +def adjust_json_from_docker(json_rep):
|
| |
+ + """If the json comes from a docker call, we need to adjust it to make use
|
| |
+ + of it. """
|
| |
+ + for item in json_rep[0]['Mounts']:
|
| |
+ + item['source'] = item['Source']
|
| |
+ + if item['Mode'] == 'rw':
|
| |
+ + item['options'] = 'rw'
|
| |
+ + if item['Mode'] == 'ro':
|
| |
+ + item['options'] = 'ro'
|
| |
+
|
| |
+ - del json_rep[0]['NetworkSettings']['Ports']
|
| |
+ + temp_ports = []
|
| |
+
|
| |
+ - json_rep[0]['NetworkSettings']['Ports'] = temp_ports
|
| |
+ + for item in json_rep[0]['NetworkSettings']['Ports']:
|
| |
+ + container_port = item.split('/')
|
| |
+ + host_port = json_rep[0]['NetworkSettings']['Ports'][item][0]['HostPort']
|
| |
+ + new_port = {'hostPort':int(host_port), 'protocol':container_port[1]}
|
| |
+ + temp_ports.append(new_port)
|
| |
+ +
|
| |
+ + del json_rep[0]['NetworkSettings']['Ports']
|
| |
+ +
|
| |
+ + json_rep[0]['NetworkSettings']['Ports'] = temp_ports
|
| |
+ +
|
| |
+ +def parse_inspect(data):
|
| |
+ + json_rep = json.loads(data)
|
| |
+ + if json_is_podman_or_docker_format(json_rep):
|
| |
+ + if not json_is_podman_format(json_rep):
|
| |
+ + adjust_json_from_docker(json_rep)
|
| |
+
|
| |
+ return json_rep
|
| |
+
|
| |
+ +def get_inspect_format(data):
|
| |
+ + json_rep = json.loads(data)
|
| |
+ + if json_is_podman_or_docker_format(json_rep):
|
| |
+ + if json_is_podman_format(json_rep):
|
| |
+ + return "podman"
|
| |
+ + return "docker"
|
| |
+ + return "CRI-O"
|
| |
+ +
|
| |
+ +def get_mounts(data, inspect_format):
|
| |
+ + if inspect_format in ['podman', 'docker']:
|
| |
+ + return data[0]['Mounts']
|
| |
+ + if inspect_format == "CRI-O":
|
| |
+ + return data['status']['mounts']
|
| |
+ + raise Exception("Error getting mounts from unknown format %s" %
|
| |
+ + inspect_format)
|
| |
+ +
|
| |
+ +def get_ports(data, inspect_format):
|
| |
+ + if inspect_format in ['podman', 'docker']:
|
| |
+ + return data[0]['NetworkSettings']['Ports']
|
| |
+ + if inspect_format == "CRI-O":
|
| |
+ + # Not applicable in the CRI-O case, since this is handled by the
|
| |
+ + # kube-proxy/CNI.
|
| |
+ + return []
|
| |
+ + raise Exception("Error getting mounts from unknown format %s" %
|
| |
+ + inspect_format)
|
| |
+ +
|
| |
+ +def get_caps(data, opts, inspect_format):
|
| |
+ + if opts['Caps']:
|
| |
+ + if opts['Caps'] == 'None':
|
| |
+ + return []
|
| |
+ + return opts['Caps'].split(',')
|
| |
+ +
|
| |
+ + if inspect_format == 'podman':
|
| |
+ + return data[0]['EffectiveCaps']
|
| |
+ + return []
|
| |
+ +
|
| |
+ def parse_cap(data):
|
| |
+ return data.decode().split('\n')[1].split(',')
|
| |
+
|
| |
+ -def parse_is_podman(data):
|
| |
+ - json_rep = json.loads(data)
|
| |
+ - return 'container=podman' in json_rep[0]['Config']['Env']
|
| |
+ -
|
| |
+ def context_to_type(context):
|
| |
+ return context.split('=')[1].split(':')[2]
|
| |
+
|
| |
+ diff --git a/udica/policy.py b/udica/policy.py
|
| |
+ index e95c634..0587372 100644
|
| |
+ --- a/udica/policy.py
|
| |
+ +++ b/udica/policy.py
|
| |
+ @@ -89,7 +89,7 @@ def list_ports(port_number, port_proto):
|
| |
+ if low <= port_number <= high and port_proto == proto_str:
|
| |
+ return ctype
|
| |
+
|
| |
+ -def create_policy(opts, capabilities, mounts, ports, append_rules):
|
| |
+ +def create_policy(opts, capabilities, mounts, ports, append_rules, inspect_format):
|
| |
+ policy = open(opts['ContainerName'] +'.cil', 'w')
|
| |
+ policy.write('(block ' + opts['ContainerName'] + '\n')
|
| |
+ policy.write(' (blockinherit container)\n')
|
| |
+ @@ -138,6 +138,74 @@ def create_policy(opts, capabilities, mounts, ports, append_rules):
|
| |
+ policy.write(' (allow process ' + list_ports(item['hostPort'], item['protocol']) + ' ( ' + perms.socket[item['protocol']] + ' ( name_bind ))) \n')
|
| |
+
|
| |
+ # mounts
|
| |
+ + if inspect_format == "CRI-O":
|
| |
+ + write_policy_for_crio_mounts(mounts, policy)
|
| |
+ + else:
|
| |
+ + write_policy_for_podman_mounts(mounts, policy)
|
| |
+ +
|
| |
+ + if append_rules != None:
|
| |
+ + for rule in append_rules:
|
| |
+ + if opts['ContainerName'] in rule[0]:
|
| |
+ + policy.write(' (allow process ' + rule[1] + ' ( ' + rule[2] + ' ( ' + rule[3] + ' ))) \n')
|
| |
+ + else:
|
| |
+ + print('WARNING: process type: ' + rule[0] + ' seems to be unrelated to this container policy. Skipping allow rule.')
|
| |
+ +
|
| |
+ + policy.write(')')
|
| |
+ + policy.close()
|
| |
+ +
|
| |
+ +
|
| |
+ +def write_policy_for_crio_mounts(mounts, policy):
|
| |
+ + for item in mounts:
|
| |
+ + if item['hostPath'].startswith('/var/lib/kubelet'):
|
| |
+ + # These should already have the right context
|
| |
+ + continue
|
| |
+ + if item['hostPath'] == LOG_CONTAINER:
|
| |
+ + if item['readonly']:
|
| |
+ + policy.write(' (blockinherit log_container)\n')
|
| |
+ + else:
|
| |
+ + policy.write(' (blockinherit log_rw_container)\n')
|
| |
+ + add_template("log_container")
|
| |
+ + continue
|
| |
+ +
|
| |
+ + if item['hostPath'] == HOME_CONTAINER:
|
| |
+ + if item['readonly']:
|
| |
+ + policy.write(' (blockinherit home_container)\n')
|
| |
+ + else:
|
| |
+ + policy.write(' (blockinherit home_rw_container)\n')
|
| |
+ + add_template("home_container")
|
| |
+ + continue
|
| |
+ +
|
| |
+ + if item['hostPath'] == TMP_CONTAINER:
|
| |
+ + if item['readonly']:
|
| |
+ + policy.write(' (blockinherit tmp_container)\n')
|
| |
+ + else:
|
| |
+ + policy.write(' (blockinherit tmp_rw_container)\n')
|
| |
+ + add_template("tmp_container")
|
| |
+ + continue
|
| |
+ +
|
| |
+ + if item['hostPath'] == CONFIG_CONTAINER:
|
| |
+ + if item['readonly']:
|
| |
+ + policy.write(' (blockinherit config_container)\n')
|
| |
+ + else:
|
| |
+ + policy.write(' (blockinherit config_rw_container)\n')
|
| |
+ + add_template("config_container")
|
| |
+ + continue
|
| |
+ +
|
| |
+ + # TODO(jaosorior): Add prefix-dir to path. This way we could call this
|
| |
+ + # from a container in kubernetes
|
| |
+ + contexts = list_contexts(item['hostPath'])
|
| |
+ + for context in contexts:
|
| |
+ + if item['readonly']:
|
| |
+ + policy.write(' (allow process ' + context + ' ( dir ( ' + perms.perm['drw'] + ' ))) \n')
|
| |
+ + policy.write(' (allow process ' + context + ' ( file ( ' + perms.perm['frw'] + ' ))) \n')
|
| |
+ + policy.write(' (allow process ' + context + ' ( sock_file ( ' + perms.perm['srw'] + ' ))) \n')
|
| |
+ + else:
|
| |
+ + policy.write(' (allow process ' + context + ' ( dir ( ' + perms.perm['dro'] + ' ))) \n')
|
| |
+ + policy.write(' (allow process ' + context + ' ( file ( ' + perms.perm['fro'] + ' ))) \n')
|
| |
+ + policy.write(' (allow process ' + context + ' ( sock_file ( ' + perms.perm['sro'] + ' ))) \n')
|
| |
+ +
|
| |
+ +
|
| |
+ +def write_policy_for_podman_mounts(mounts, policy):
|
| |
+ for item in mounts:
|
| |
+ if not item['Source'].find("/"):
|
| |
+ if (item['Source'] == LOG_CONTAINER and item['RW'] is False):
|
| |
+ @@ -191,16 +259,6 @@ def create_policy(opts, capabilities, mounts, ports, append_rules):
|
| |
+ policy.write(' (allow process ' + context + ' ( file ( ' + perms.perm['fro'] + ' ))) \n')
|
| |
+ policy.write(' (allow process ' + context + ' ( sock_file ( ' + perms.perm['sro'] + ' ))) \n')
|
| |
+
|
| |
+ - if append_rules != None:
|
| |
+ - for rule in append_rules:
|
| |
+ - if opts['ContainerName'] in rule[0]:
|
| |
+ - policy.write(' (allow process ' + rule[1] + ' ( ' + rule[2] + ' ( ' + rule[3] + ' ))) \n')
|
| |
+ - else:
|
| |
+ - print('WARNING: process type: ' + rule[0] + ' seems to be unrelated to this container policy. Skipping allow rule.')
|
| |
+ -
|
| |
+ - policy.write(')')
|
| |
+ - policy.close()
|
| |
+ -
|
| |
+ def load_policy(opts):
|
| |
+ PWD = getcwd()
|
| |
+ chdir(TEMPLATES_STORE)
|
| |
Run Fedora CI runs on new patch for udica to support CRI-O