#12 Test CRIO support via downstream CI testsuite
Closed 2 years ago by vmojzis. Opened 4 years ago by lvrabec.
rpms/ lvrabec/udica fb-crio  into  rawhide

@@ -0,0 +1,49 @@ 

+ From db10deb2c3fa6e3059f6255439e8404064a18aa2 Mon Sep 17 00:00:00 2001

+ From: Juan Antonio Osorio Robles <jaosorior@redhat.com>

+ Date: Tue, 17 Sep 2019 15:59:11 +0300

+ Subject: [PATCH 1/2] Improve permission set for log_container template

+ 

+ For the log_rw_container it wasn't possible to create new files, which

+ is something that's normally required. So we're adding this

+ capability, while still not allowing that container to rename that

+ directory or remove files from it as a security measure.

+ 

+ The audit_log_t file was also modified to be more restrictive for the

+ log_rw_container block, so we only allow reads now. However, the write

+ capability was left for the log_manage_container block.

+ ---

+  udica/templates/log_container.cil | 11 ++++++-----

+  1 file changed, 6 insertions(+), 5 deletions(-)

+ 

+ diff --git a/udica/templates/log_container.cil b/udica/templates/log_container.cil

+ index 2e22437..767c669 100644

+ --- a/udica/templates/log_container.cil

+ +++ b/udica/templates/log_container.cil

+ @@ -12,12 +12,12 @@

+  (block log_rw_container

+      (blockinherit log_container)

+  

+ -    (allow process logfile (dir (getattr search open)))

+ -    (allow process logfile (file (ioctl read write getattr lock append open)))

+ +    (allow process logfile (dir (ioctl read write create getattr setattr lock add_name search open)))

+ +    (allow process logfile (file (ioctl read write create getattr setattr lock append open)))

+      (allow process logfile (lnk_file (ioctl read write getattr lock append open)))

+      (allow process var_t (dir (getattr search open)))

+ -    (allow process auditd_log_t (dir (ioctl read write getattr lock search open)))

+ -    (allow process auditd_log_t (file (ioctl read write getattr lock open)))

+ +    (allow process auditd_log_t (dir (ioctl read getattr lock search open)))

+ +    (allow process auditd_log_t (file (ioctl read getattr lock open)))

+  )

+  

+  (block log_manage_container

+ @@ -26,5 +26,6 @@

+      (allow process logfile (dir (ioctl read write create getattr setattr lock unlink link rename add_name remove_name reparent search rmdir open)))

+      (allow process logfile (file (ioctl read write create getattr setattr lock append unlink link rename open)))

+      (allow process logfile (lnk_file (ioctl read write create getattr setattr lock append unlink link rename)))

+ -

+ +    (allow process auditd_log_t (dir (ioctl read write getattr lock search open)))

+ +    (allow process auditd_log_t (file (ioctl read write getattr lock open)))

+  )

+ -- 

+ 2.21.0

+ 

@@ -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)

file modified
+3 -1
@@ -1,7 +1,7 @@ 

  Summary: A tool for generating SELinux security policies for containers

  Name: udica

  Version: 0.1.9

- Release: 1%{?dist}

+ Release: 1%{?dist}.crio.1

  Source0: https://github.com/containers/udica/archive/v%{version}.tar.gz

  License: GPLv3+

  BuildArch: noarch
@@ -14,6 +14,8 @@ 

  Requires: python2 libsemanage-python libselinux-python

  %endif

  patch01: 0001-Update-tests-test_basic.podman.cil-test_basic.docker.patch

+ patch02: 0001-Improve-permission-set-for-log_container-template.patch

+ patch03: 22c297b322ddc46eb9cd1f96cb598929e8de607e.patch

  

  %description

  Tool for generating SELinux security profiles for containers based on