- Published on
- // 9 min read
Open source threat intelligence and SELinux
- Authors
- Name
- Shane Boulden
- @shaneboulden
MISP is an awesome open source project, allowing security analysts to gather, share, store and correlate indicators of compromise (IoC) using open standards. But how do you protect a platform designed to support security analysts?
In this post, I'll deploy a containerised MISP instance. Once deployed, we'll configure SELinux to protect our application containers. We'll observe how SELinux prevents access to certain files initially, and then create SELinux policies with Udica to only allow access to resources required by the platform.
SELinux and containers
SELinux started life as a series of patches to the Linux kernel. Since being released to the open source community, it's been an integral security control for Linux systems. Extensions have been created supporting different use cases, like Multi-level security (MLS).
SELinux alone has mitigated several high-priority vulnerabilities impacting containers. One of these was CVE-2019-5736, which allowed containers to overwrite file handles inside the container and 'escape' to the host, with admin privileges. Fortunately, a well-configured SELinux deployment completely mitigated this CVE!
Getting started
I'll be using Red Hat Enterprise Linux 8 for this post, and you can get a copy at Red Hat Developers.
Once you have a Red Hat Enterprise Linux instance up, subscribe it (you don't need to do this on AWS with pay-as-you-go instances) and make sure all the packages are updated.
subscription-manager register
subscription-manager attach --auto
yum update -y
Firstly, let's install podman
. Podman is an open source, drop-in replacement for Docker on Red Hat Enterprise Linux.
yum -y install podman podman-plugins podman-docker
We'll also be using docker-compose
to orchestrate the MISP platform, which you can grab from GitHub
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Configuration
We're using the dnsname
plugin here to provide local DNS for the container services. First, we need to update the default network configuration. Add the following to /etc/cni/net.d/87-podman-bridge.conflist
:
cat /etc/cni/net.d/87-podman-bridge.conflist
{
"cniVersion": "0.4.0",
"name": "podman",
"plugins": [
{
...
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}
Start dnsmasq
and the Podman socket, and verify the dnsname
plugin is active:
systemctl start {dnsmasq,podman.socket}
podman network ls
2f259bab93aa podman 0.4.0 bridge,portmap,firewall,tuning,dnsname
Create the docker-compose.yml
file, and start the services. Optionally, if you're deploying this on a remote host, you can update the HOSTNAME
environment variable for the misp
service in the compose file.
curl https://gist.githubusercontent.com/shaneboulden/3a4fc946ce78ad647511610870477625/raw/0cc8498afb4df5db8f98a8a6f4e964aa98a33b8a/misp-docker-compose-nolabels.yml -o docker-compose.yml
An important note - this Dockerfile has been modified from the one hosted at https://github.com/coolacid/docker-misp. Specifically, we've added ports to each service:
mail:
image: namshi/smtp
ports:
- "25:25"
This allows us to generate SELinux policies in the next section. But, it also exposes the ports outside the Podman network unnecessarily. We'll correct this later in the article.
Testing it out
At this point you can bring all the services up
docker-compose up
You'll start to see some services deploying, and probably a few messages like this:
misp_1 | Setup MISP files dir...
misp_1 | cp: cannot stat '/var/www/MISP/app/files/community-metadata/defaults.json': Permission denied
misp_1 | cp: cannot stat '/var/www/MISP/app/files/empty': Permission denied
misp_1 | cp: cannot stat '/var/www/MISP/app/files/feed-metadata/defaults.json': Permission denied
misp_1 | cp: cannot stat '/var/www/MISP/app/files/feed-metadata/schema.json': Permission denied
misp_1 | cp: cannot stat '/var/www/MISP/app/files/misp-decaying-models/models/nids-simple-model.json': Permission denied
misp_1 | cp: cannot stat '/var/www/MISP/app/files/misp-decaying-models/models/phishing-model.json': Permission denied
It looks like something is preventing the containerised application accessing a file. Maybe SELinux? Let's run a command to see:
ausearch -m avc -ts recent
Yep, that looks like SELinux. You'll see a number of SELinux denials that look like this:
----
type=AVC msg=audit(1630132075.918:2462): avc: denied { setattr } for pid=5208 comm="rsync" name="wikimedia" dev="dm-0" ino=33602346 scontext=system_u:system_r:container_t:s0:c116,c220 tcontext=system_u:object_r:admin_home_t:s0 tclass=dir permissive=0
----
type=AVC msg=audit(1630132075.918:2463): avc: denied { setattr } for pid=5208 comm="rsync" name="tools" dev="dm-0" ino=50918545 scontext=system_u:system_r:container_t:s0:c116,c220 tcontext=system_u:object_r:admin_home_t:s0 tclass=dir permissive=0
Creating SELinux policy with Udica
At this point our application starts, but can't access all the files and ports it needs. We need to create policies for the containers so SELinux will allow access to the files and ports they need - and only the files and ports they need!
Udica is a tool for creating SELinux policies from container specs. It determines which ports and files a container needs, and creates SELinux policies allowing access. Udica is available in the Red Hat Enterprise Linux 8 AppStream repos:
yum install -y /usr/bin/udica
Firstly, we need to inspect our containers:
podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
...
81739f877c42 docker.io/namshi/smtp:latest exim -bd -q15m -v 3 days ago Exited (1) 3 minutes ago 0.0.0.0:25->25/tcp 81739f877c42_root_mail_1
podman inspect 81739f877c42 > misp-mail.json
Once we have a json spec for the container, we can create an SELinux policy with Udica:
udica -j misp-mail.json misp-mail
You'll now see some output from Udica indicating you can load the policy. Let's try it out:
semodule -i misp-mail.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
Ok! Now repeat this same process for each of the other containers: inspect the container with podman, create a policy with Udica, and load it.
You'll also need to update each service in the docker-compose.yml
file with SELinux labels, which look like label=type:your-process-label.process
. Now is also a good time to remove the port definitions we used in development to create the SELinux policies. You can find a templated compose file here:
curl https://gist.githubusercontent.com/shaneboulden/43909547297482e39268ef42212797f0/raw/37d2d48e42b58e22324ecf07171eb97f45c0a55a/misp-docker-compose.yml -o docker-compose.yml
Once you've updated the compose file, you can restart the services
docker-compose down
docker-compose up
Building on the Udica policies
Ok, now our MISP instance is starting, but we still have an issue. The application can't connect to the database:
misp_1 | Waiting for database to come up
misp_1 | ERROR 2002 (HY000): Can't connect to MySQL server on 'db' (13)
Let's take another look at the audit log:
ausearch -m avc -ts recent
type=AVC msg=audit(1630213249.039:2914): avc: denied { name_connect } for pid=19132 comm="mysql" dest=3306 scontext=system_u:system_r:misp-docker.process:s0:c296,c415 tcontext=system_u:object_r:mysqld_port_t:s0 tclass=tcp_socket permissive=0
It looks like SELinux is still preventing the application connecting to the database. Udica did its best to create an SELinux policy, and we just need to give it a helping hand. Let's append this denial to the misp-docker
policy:
cat avc
type=AVC msg=audit(1630213249.039:2914): avc: denied { name_connect } for pid=19132 comm="mysql" dest=3306 scontext=system_u:system_r:misp-docker.process:s0:c296,c415 tcontext=system_u:object_r:mysqld_port_t:s0 tclass=tcp_socket permissive=0
podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
...
3bc4ab0a365d docker.io/coolacid/misp-docker:core-latest 2 minutes ago Exited (137) About a minute ago 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp root_misp_1
podman inspect 3bc4ab0a365d | udica --append-rules avc misp-docker
semodule -i misp-docker.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
If you restart the container services you should now be able to see the application connect to the database:
docker-compose down
docker-compose up
A little more tweaking
We're making progress. The database connects, but there's still a few issues with permissions for the remaining services
misp_1 | Welcome to CakePHP v2.10.24 Console
misp_1 | ---------------------------------------------------------------
misp_1 | App : app
misp_1 | Path: /var/www/MISP/app/
misp_1 | ---------------------------------------------------------------
misp_1 | Error: Permission denied
Looking at the audit logs shows a familiar situation:
ausearch -m avc -ts recent
type=AVC msg=audit(1630213508.657:3152): avc: denied { name_connect } for pid=21088 comm="php" dest=6666 scontext=system_u:system_r:misp-docker.process:s0:c22,c509 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
We could keep repeating this process, but for the sake of brevity I've captured most of these errors into a couple of files we can load:
curl https://gist.githubusercontent.com/shaneboulden/da4c79c92f8c329448705ce9c3372c2b/raw/28d2966d6dd0687ff149d769f3ac5ed49fca19bc/avc-misp-modules -o avc-misp-modules
curl https://gist.githubusercontent.com/shaneboulden/bff9b8e0297d3d41bdc41e3de9f32641/raw/fdee6a0a890d641d8f0b0b27ed2340b685f86c57/avc-misp-docker -o avc-misp-docker
podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
732bf416a5ef docker.io/coolacid/misp-docker:modules-latest 7 minutes ago Exited (0) 4 minutes ago root_misp-modules_1
118d53ade322 docker.io/coolacid/misp-docker:core-latest 7 minutes ago Exited (137) 4 minutes ago 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp root_misp_1
podman inspect 732bf416a5ef | udica --append-rules avc-misp-modules misp-modules
semodule -i misp-modules.cil /usr/share/udica/templates/base_container.cil
podman inspect 118d53ade322 | udica --append-rules avc-misp-docker misp-docker
semodule -i misp-docker.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
docker-compose down
docker-compose up -d
If we have a look at the audit logs now
ausearch -m avc -ts recent
<no matches>
Success! We've now created SELinux policies for our containerised, open source threat intelligence platform.
Closing out
We've deployed an open source threat intelligence platform in containers to Red Hat Enterprise Linux and created customised SELinux policies for our deployment. Now we can rest assured that SELinux is mitigating some of the nastier container escape exploits that could compromise the system.
Udica made creating the policies easier after a bit of tweaking. But our application still has some weaknesses - it's only deployed to a single host, with no reduncancy, and updating the application will likely cause an outage for security analysts. In a later post, I'll deploy this same application to an enterprise Kubernetes platform, OpenShift. This supports scale out for application components, fail-over to worker nodes, and minimal downtime update strategies, like 'blue-green' or 'canary' deployments. We'll also see how OpenShift handles SELinux policies for the application transparently - SELinux will still protect the application resources, but we won't need to get into the weeds of creating these with Udica.
If you'd like to read a little more, there's an article here on SELinux multi-category security, which OpenShift uses.
The application is also running as root, and there's a few ways we could resolve this:
- Use rootless containers with podman
- Deploy the application to OpenShift, which by default doesn't permit applications to run as root
I'll explore the second option here when we deploy this application to OpenShift. Until next time!