Docker, go and privileged ports

Dominic
elbstack
Published in
2 min readApr 4, 2019

We at elbstack recently started to work on a microservices system for our internal tooling. And of course, we wanted most of the surroundings to be as automated as possible, starting by automated tests and of course automatic deploys for each service. As we work with several different languages (javascript/node, golang, php, … ) and a variety of frameworks at elbstack, we needed something consistent for all services. We do not want to adjust the build pipeline every time a new service is created.

To accomplish this, we use docker with one requirement:

Serve an endpoint on port 80. (Also the endpoint should be a grapqhl endpoint, but this is not part of this post).

There is only one problem: to run services with ports beneath 1024, which are called privileged ports, you need root privileges. Even in a restricted virtualized environment like docker you don’t want services to run with root privileges.

But there is another way: setcap. setcap is a command in Linux systems to set specific capabilities for a file. The list of capabilities starts with the permission to control kernel auditing (CAP_AUDIT_CONTROL), permission to set uid and gid of other files like the chown command (CAP_CHOWN) and goes on with the privileges to kill other processes (CAP_KILL) and a lot more. The one which we need is CAP_NET_BIND.

We use the following Dockerfile to get one of our services, written in go, running on port 80.

The most relevant part of the dockerfile for this post is the RUN setcap 'cap_net_bind_service=+ep' /app command. As mentioned, the capability cap_net_bind allows our app to run with ports beneath 1024. The flags mean: e=activated , p=permitted . You can add i to allow the process to also spawn child processes which open privileged ports.

Finally we need an unprivileged user which runs our app which can be accomplished by these commands:

...
RUN
addgroup -S -g 10101 goapp
RUN adduser -S -D -u 10101 -s /sbin/nologin -h /goapp -G goapp goapp

RUN chown -R goapp:goapp /app /src
...
COPY --from=builder /etc/group /etc/passwd /etc/
...
USER
goapp

There are some additional commands in there which I just learned recently.

ARG GO_VERSION=1.12 allows you to define a default value which can be overridden from the outside like docker build --build-arg GO_VERSION=1.11 ... .

The AS in FROM golang:${GO_VERSION}-alpine AS builder defines a new named stage in the build process. In combination with FROM scratch AS final and COPY --from=builder … it is almost like resetting the whole build to start and copy the relevant parts without all the source files etc. The resulting docker image will be a lot smaller.

--

--