Openshift comes with enforced security context design which aims to solve security issues that normal Kubernetes cluster ignores. In a non-prod environment, the default Kubernetes approach is capible to deploy simple application and providing access to the service, but such design often introduce challenges to enterprise companies like banks or teleco which cause them hasitate to migrate data to the cloud.

Build A Openshift Compatible Image

Normal docker image which uses root level action like following would cause trouble in Openshift:

FROM ubuntu:18.04

RUN echo "echo 'Starting Shell Script...'" >
RUN echo "rm /etc/passwd && echo '-> execution of rm command successful.'
|| '-> execution of rm command failed.'" >>
RUN echo "echo 'Ending Shell Script...'" >>
RUN chmod +x

CMD [ "sh", "" ]

The problem here is that rm /etc/passwd tries to delete root file passwd, in a local docker or Kubernetes default enviroment, this action is fine to use, because Docker doesn’t care and Kubernetes default image runuser is root. But in Openshift, the default runuser is non-root.

And it’s why when you try to run it in Openshift, it’ll give error:

oc get pods
 NAME                READY   STATUS             RESTARTS   AGE
 root-app-1-lljqm    0/1     CrashLoopBackOff   1          5s

Starting Shell Script...
 rm: cannot remove '/etc/passwd': Permission denied 2: -> execution of rm command failed.: not found
 Ending Shell Script...

This enforces user to build up a possitive habit to properly secure their images from very low lever. It means that you need to firstly assess whether your container image requires root access and modify the image not to run as root. If the image does not require any root user access, the best practice is to specify a USER who is non-root in your Dockerfile as shown.

FROM <base-image>
USER <user-id>
CMD [.., ...]

Pods run in an OpenShift cluster as arbitrary user IDs. All of these user IDs are members of the root group. Any files user creates will belong to root Group, the solution is to change ownership of folder users interact with.

## 4. Two-stage image builds (stage 1: builder image)
FROM maven:3.6.3-jdk-11 as builder
COPY pom.xml .
RUN mvn -e -B dependency:resolve
COPY src ./src
RUN mvn clean -e -B package

## 4. Two-stage image builds (stage 2: deployment image)
## 1. Universal Base Image (UBI)

## 2. Non-root, arbitrary user IDs
USER 1001  # Or USER default; or nothing, the UBI already set the user

## 6. Image identification
LABEL name="my-namespace/my-image-name" \
      vendor="My Company, Inc." \
      version="1.2.3" \
      release="45" \
      summary="Web search application" \
      description="This application searches the web for interesting stuff."

USER root

## 7. Image license
COPY ./licenses /licenses

## 5. Latest security updates
RUN dnf -y update-minimal --security --sec-severity=Important --sec-severity=Critical && \
    dnf clean all

USER default  # Or USER 1001

## 3. Group ownership and file permission, make is usable for both openshift and kubernetes
RUN chown -R 1001:0 /some/directory && \
    chmod -R g=u /some/directory

COPY --from=builder /app/target/*.jar app.jar
CMD ["java", "-jar", "app.jar"]

Two Stage Image Builds

From the above case example we can see here the problem/security concern is all caused by user interaction with file system. If we can seperate them from basic image actions, then we don’t need to worry about compatibility and security anymore. Multi-stage image builds is one of the solutions for this purpose. We can make all user interaction in the base image, and then import it in next image which only includes app run related cmds and build/run on cloud.

FROM golang:1.7.3 AS builder
WORKDIR /go/src/
RUN go get -d -v  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/ .
CMD ["./app"]