We will use CentOS 7 as the operating system for the node labs in this book, unless otherwise indicated. We will install Docker Community Edition now and Docker Enterprise for the specific chapters pertaining to this platform.
Deploy environments/standalone-environment from this book's GitHub repository (https://github.com/PacktPublishing/Docker-Certified-Associate-DCA-Exam-Guide.git) if you have not done so yet. You can use your own CentOS 7 server. Use vagrant up from the environments/standalone-environment folder to start your virtual environment.
If you are using a standalone environment, wait until it is running. We can check the statuses of the nodes using vagrant status. Connect to your lab node using vagrant ssh standalone. standalone is the name of your node. You will be using the vagrant user with root privileges using sudo. You should get the following output:
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant up
Bringing machine 'standalone' up with 'virtualbox' provider...
==> standalone: Cloning VM...
==> standalone: Matching MAC address for NAT networking...
==> standalone: Checking if box 'frjaraur/centos7' version '1.4' is up to date...
==> standalone: Setting the name of the VM: standalone
...
==> standalone: Running provisioner: shell...
standalone: Running: inline script
standalone: Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant status
Current machine states:
standalone running (virtualbox)
...
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$
We can now connect to a standalone node using vagrant ssh standalone. This process may vary if you've already deployed a standalone virtual node before and you just started it using vagrant up:
Docker-Certified-Associate-DCA-Exam-Guide/environments/standalone$ vagrant ssh standalone
[vagrant@standalone ~]$
Now, you are ready to start the labs.
Installing the Docker runtime and executing a "hello world" container
This lab will guide you through the Docker runtime installation steps and running your first container. Let's get started:
- To ensure that no previous versions are installed, we will remove any docker* packages:
[vagrant@standalone ~]$ sudo yum remove docker*
- Add the required packages by running the following command:
[vagrant@standalone ~]$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
- We will be using a stable release, so we will add its package repository, as follows:
[vagrant@standalone ~]$ sudo yum-config-manager \
--add-repo https://download.docker.com/linux/centos/docker-ce.repo
- Now, install Docker packages and containerd. We are installing the server and client on this host (since version 18.06, Docker provides different packages for docker-cli and Docker daemon):
[vagrant@standalone ~]$ sudo yum install -y docker-ce docker-ce-cli containerd.io
- Docker is installed, but on Red Hat-like operating systems, it is not enabled on boot by default and will not start. Verify this situation and enable and start the Docker service:
[vagrant@standalone ~]$ sudo systemctl enable docker
[vagrant@standalone ~]$ sudo systemctl start docker
- Now that Docker is installed and running, we can run our first container:
[vagrant@standalone ~]$ sudo docker container run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest:
sha256:b8ba256769a0ac28dd126d584e0a2011cd2877f3f76e093a7ae560f2a5301c00
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64)
3. The Docker daemon created a new container from that image that runs the executable, which produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/.
For more examples and ideas, visit:
https://docs.docker.com/get-started/.
This command will send a request to Docker daemon to run a container based on the hello-world image, located on Docker Hub (http://hub.docker.com). To use this image, Docker daemon downloads all the layers if we have not executed any container with this image before; in other words, if the image is not present on the local Docker host. Once all the image layers have been downloaded, Docker daemon will start a hello-world container.
- As you should have noticed, we are always using sudo to root because our user has not got access to the Docker UNIX socket. This is the first security layer an attacker must bypass on your system. We usually enable a user to run containers in production environments because we want to isolate operating system responsibilities and management from Docker. Just add our user to the Docker group, or add a new group of users with access to the socket. In this case, we will just add our lab user to the Docker group:
[vagrant@standalone ~]$ docker container ls
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json
: dial unix /var/run/docker.sock: connect: permission denied
[vagrant@standalone ~]$ sudo usermod -a -G docker $USER
[vagrant@standalone ~]$ newgrp docker
[vagrant@standalone ~]$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5f7abd49b3e7 hello-world "/hello" 19 minutes ago Exited (0) 19 minutes ago festive_feynman
Docker runtime processes and namespace isolation
In this lab, we are going to review what we learned about process isolation and Docker daemon components and execution workflow. Let's get started:
- Briefly review the Docker systemd daemon:
[vagrant@standalone ~]$ sudo systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: active (running) since sáb 2019-09-28 19:34:30 CEST; 25min ago
Docs: https://docs.docker.com
Main PID: 20407 (dockerd)
Tasks: 10
Memory: 58.9M
CGroup: /system.slice/docker.service
└─20407 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.222200934+02:00" level=info msg="[graphdriver] using prior storage driver: overlay2"
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.234170886+02:00" level=info msg="Loading containers: start."
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.645048459+02:00" level=info msg="Default bridge (docker0) is assigned with an IP a... address"
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.806432227+02:00" level=info msg="Loading containers: done."
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.834047449+02:00" level=info msg="Docker daemon" commit=6a30dfc graphdriver(s)=over...n=19.03.2
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.834108635+02:00" level=info msg="Daemon has completed initialization"
sep 28 19:34:30 centos7-base dockerd[20407]: time="2019-09-28T19:34:30.850703030+02:00" level=info msg="API listen on /var/run/docker.sock"
sep 28 19:34:30 centos7-base systemd[1]: Started Docker Application Container Engine.
sep 28 19:34:43 centos7-base dockerd[20407]: time="2019-09-28T19:34:43.558580560+02:00" level=info msg="ignoring event" module=libcontainerd namespace=mo...skDelete"
sep 28 19:34:43 centos7-base dockerd[20407]: time="2019-09-28T19:34:43.586395281+02:00" level=warning msg="5f7abd49b3e75c58922c6e9d655d1f6279cf98d9c325ba2d3e53c36...
This output shows that the service is using a default systemd unit configuration and that dockerd is using the default parameters; that is, it's using the file descriptor socket on /var/run/docker.sock and the default docker0 bridge interface.
- Notice that dockerd uses a separate containerd process to execute containers. Let's run some containers in the background and review their processes. We will run a simple alpine with an nginx daemon:
[vagrant@standalone ~]$ docker run -d nginx:alpine
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
9d48c3bd43c5: Already exists
1ae95a11626f: Pull complete
Digest: sha256:77f340700d08fd45026823f44fc0010a5bd2237c2d049178b473cd2ad977d071
Status: Downloaded newer image for nginx:alpine
dcda734db454a6ca72a9b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7
- Now, we will look for the nginx and containerd processes (process IDs will be completely different on your system; you just need to understand the workflow):
[vagrant@standalone ~]$ ps -efa|grep -v grep|egrep -e containerd -e nginx
root 15755 1 0 sep27 ? 00:00:42 /usr/bin/containerd
root 20407 1 0 19:34 ? 00:00:02 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 20848 15755 0 20:06 ? 00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/dcda734db454a6ca72a9
b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
root 20863 20848 0 20:06 ? 00:00:00 nginx: master process nginx -g daemon off;
101 20901 20863 0 20:06 ? 00:00:00 nginx: worker process
- Notice that, at the end, the container started from 20848 PID. Following the runtime-runc location, we discover state.json, which is the container state file:
[vagrant@standalone ~]$ sudo ls -laRt /var/run/docker/runtime-runc/moby
/var/run/docker/runtime-runc/moby:
total 0
drwx--x--x. 2 root root 60 sep 28 20:06 dcda734db454a6ca72a9b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7
drwx------. 3 root root 60 sep 28 20:06 .
drwx------. 3 root root 60 sep 28 13:42 ..
/var/run/docker/runtime-runc/moby/dcda734db454a6ca72a9b9eef98aae6aefaa6f9b768a7d53bf30665d8ff70fe7:
total 28
drwx--x--x. 2 root root 60 sep 28 20:06 .
-rw-r--r--. 1 root root 24966 sep 28 20:06 state.json
drwx------. 3 root root 60 sep 28 20:06 ..
This file contains container runtime information: PID, mounts, devices, capabilities applied, resources, and more.
- Our NGINX server runs under PID 20863 and the nginx child process with PID 20901 on the Docker host, but let's take a look inside:
[vagrant@standalone ~]$ docker container exec dcda734db454 ps -ef
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
6 nginx 0:00 nginx: worker process
7 root 0:00 ps -ef
Using docker container exec, we can run a new process using a container namespace. This is like running a new process inside the container.
As you can observe, inside the container, nginx has PID 1 and it is the worker process parent. And, of course, we see our command, ps -ef, because it was launched using its namespaces.
We can run other containers using the same image and we will obtain the same results. Processes inside each container are isolated from other containers and host processes, but users on the Docker host will see all the processes, along with their real PIDs.
- Let's take a look at nginx process namespaces. We will use the lsns command to review all the host-running process's namespaces. We will obtain a list of all running processes and their namespaces. We will look for nginx processes (we will not use grep to filter the output because we want to read the headers):
[vagrant@standalone ~]$ sudo lsns
NS TYPE NPROCS PID USER COMMAND
..............
..............
4026532197 mnt 2 20863 root nginx: master process nginx -g daemon off
4026532198 uts 2 20863 root nginx: master process nginx -g daemon off
4026532199 ipc 2 20863 root nginx: master process nginx -g daemon off
4026532200 pid 2 20863 root nginx: master process nginx -g daemon off
4026532202 net 2 20863 root nginx: master process nginx -g daemon off
This lab demonstrated process isolation within a process running inside containers.
Docker capabilities
This lab will cover seccomp capability management. We will launch containers using dropped capabilities to ensure that, by using seccomp to avoid some system calls, processes in containers will only execute allowed actions. Let's get started:
- First, run a container using the default allowed capabilities. During the execution of this alpine container, we will change the ownership of the /etc/passwd file:
[vagrant@standalone ~]$ docker container run --rm -it alpine sh -c "chown nobody /etc/passwd; ls -l /etc/passwd"
-rw-r--r-- 1 nobody root 1230 Jun 17 09:00 /etc/passwd
As we can see, there is nothing to stop us from changing whatever file ownership resides inside the container's filesystem because the main process (in this case, /bin/sh) runs as the root user.
- Drop all the capabilities. Let's see what happens:
[vagrant@standalone ~]$ docker container run --rm -it --cap-drop=ALL alpine sh -c "chown nobody /etc/passwd; ls -l /etc/passwd"
chown: /etc/passwd: Operation not permitted
-rw-r--r-- 1 root root 1230 Jun 17 09:00 /etc/passwd
You will observe that the operation was forbidden. Since containers run without any capabilities, the chown command is not allowed to change file ownership.
- Now, just add the CHOWN capability to allow a change of ownership for files inside the container:
[vagrant@standalone ~]$ docker container run --rm -it --cap-drop=ALL --cap-add CHOWN alpine sh -c "chown nobody /etc/passwd; ls -l /etc/passwd"
-rw-r--r-- 1 nobody root 1230 Jun 17 09:00 /etc/passwd