Security using Docker Labels and Calico Policy
Background
With Calico as a Docker network plugin, Calico can be configured to extract the labels on a container and apply them to the workload endpoint for use with Calico policy. By default, Calico blocks all traffic unless it has been explicitly allowed through configuration of the globally defined policy which uses selectors to determine which subset of the policy is applied to each container based on their labels. This approach provides a powerful way to group together all of your network Policy, makes it easy to reuse policy in different networks, and makes it easier to define policy that extends across different orchestration systems that use Calico.
When Calico is configured to use container labels, profiles are not created and have no impact on any container traffic.
Enabling Docker Networking Container Labels Policy
To enable labels to be used in Policy selectors the flag
--use-docker-networking-container-labels
must be passed when starting
calico/node
with the calicoctl node run
command. All calico/node
instances
should be started with the flag to avoid a mix of labels and profiles.
Managing Calico policy for a network
This section provides an example applying policy using the approach described above once container labels are enabled.
We create a Calico-Docker network and use the calicoctl
tool to set policies
that achieve the required isolation and allowances.
For the example let’s assume that we want to provide the following isolation between a set of database containers and a set of frontend containers:
- Frontend containers can only access the Database containers over TCP to port 3306. For now we’ll assume no other connectivity is allowed to/from the frontend.
- Database containers have no isolation between themselves (to handle synchronization within a cluster). This could be improved by locking down the port ranges and protocols, but for brevity we’ll just allow full access between database containers.
Global policy applied through label selection
This example demonstrates using global selector-based policy with labels extracted from the Docker containers.
1. Create the Docker network
On any host in your Calico / Docker network, run the following command:
docker network create --driver calico --ipam-driver calico-ipam net1
2. Create the Labeled Workloads
We set labels on each container indicating the role, in our case frontend
or database. The labels are applied directly to each container and must be
prefixed with org.projectcalico.label.
for them to be extracted and applied
to the workload endpoint.
We have decided to use the label role
indicating the role and a value of
either frontend
or database
.
Create the workloads as docker containers with appropriate labels.
docker run --label org.projectcalico.label.role=frontend --net net1 --name frontend-A -tid busybox
docker run --label org.projectcalico.label.role=database --net net1 --name database-A -tid busybox
3. Create policy
Create the global policy to provide the required network isolation.
Policy resources are defined globally, and include a set of ingress and egress rules and actions, where each rule can filter packets based on a variety of source or destination attributes (which includes selector based filtering using label selection).
Each policy resource also has a “main” selector that is used to determine which endpoints the policy is applied to based on the applied labels.
We can use calicoctl create
to create two new policies for this:
cat << EOF | calicoctl create -f -
- apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: database
spec:
order: 0
selector: role == 'database'
ingress:
- action: allow
protocol: tcp
source:
selector: role == 'frontend'
destination:
ports:
- 3306
- action: allow
source:
selector: role == 'database'
egress:
- action: allow
destination:
selector: role == 'database'
- apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: frontend
spec:
order: 0
selector: role == 'frontend'
egress:
- action: allow
protocol: tcp
destination:
selector: role == 'database'
ports:
- 3306
EOF
This works as follows:
- Each database container is given the label
role = database
. - Each frontend container in given the label
role = frontend
. - The global policy resource “database” uses the selector
role == database
to select containers with labelrole = database
and applies ingress and egress policy:- An ingress rule to allow TCP traffic to port 3306 from endpoints that have
the label
role = frontend
(i.e. from frontend containers since they are the only ones with the labelrole = frontend
) - An ingress and egress rule to allow all traffic from and to endpoints that
have the label
role = database
(i.e. from database containers).
- An ingress rule to allow TCP traffic to port 3306 from endpoints that have
the label
- The global policy resource “frontend” uses the selector
role == frontend
to select containers with labelrole = frontend
and applies a single egress rule to allow all TCP traffic to port 3306 on endpoints that have the labelrole = database
(i.e. to database containers)
For details on all of the possible match criteria, see the policy resource documentation.
Multiple networks
While some network providers tend to use multiple networks to enforce isolation, Calico instead opts to put all containers in the same, flat network, where they are separated by default, and then connect them using Calico policy. For this reason, Calico does not support attaching a container to multiple docker networks.
Extending the previous example, suppose we introduce another label that is used for system backups and that we want some of our database containers to have the database label and a backup label (so they have database policy and backup policy applied).
One approach for doing this is as follows:
- Define a new label, say
backup = true
to indicate that a particular endpoint should be allowed access to the backup network. - Define global policy “backupnetwork” that allows full access between all
components with the
backup = true
label.
For your database containers that also need to be able to access the backup
endpoints, launch them assigning both the role = database
and backup = true
labels.
docker run --label org.projectcalico.label.role=database --label org.projectcalico.label.backup=true --net net1 --name database-B -tid busybox
Since containers started like this will have the two labels assigned to them, they will pick up policy that selects both labels - in other words they will have the locked down database access plus access to the backup network.
Obviously, the example of allowing full access between everything on the “backup”
network is probably a little too permissive, so you can lock down the access within
the backup network by modifying the global policy selected by the backup = true
label.