Skip to main content

Seguridad de Imágenes

Haz un breve repaso en cka container and images.

Una cosa muy importante es entender que los contenedores son un grupo de capas siendo que solamente la última capa superior es escribible. Las capas anteriores son solo lectura.

La mayoría de las veces creamos una imagen a partir de otra imagen como base. Estamos importando todas las capas que ella posee y añadiendo nuevas capas.

No todo comando en el Dockerfile crea una capa, solamente los comandos RUN, COPY y ADD. Cada vez que ponemos una nueva capa están aumentando el tamaño de la imagen.

FROM ubuntu  # IMPORTANDO LAS CAPAS QUE EXISTEN EN ESTA IMAGEN

RUN apt-get update & apt-get install -y golang-go # AÑADIENDO ESTA CAPA

CMD ["sh"]

¿Cómo reducir el tamaño de una imagen?

Vamos a imaginar que estamos queriendo instalar curl y usando la imagen de nginx como base.

Lo que tenemos es esto...

alt text

Vamos a partir del siguiente Dockerfile y del app.go que sería una posible aplicación.

root@cks-master:~# mkdir app && cd app
root@cks-master:~/app# vim Dockerfile
root@cks-master:~/app# cat Dockerfile
FROM ubuntu
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go
CMD ["./app"]
root@cks-master:~/app# vim app.go
root@cks-master:~/app# cat app.go
package main

import (
"fmt"
"time"
"os/user"
)

func main () {
user, err := user.Current()
if err != nil {
panic(err)
}

for {
fmt.Println("user: " + user.Username + " id: " + user.Uid)
time.Sleep(1 * time.Second)
}
}
root@cks-master:~/app#

Es una aplicación simple con un bucle infinito imprimiendo usuario, id y userid.

Si hacemos el build de esto y ejecutarlo mira el tamaño de la imagen, vamos a ejecutar y ver qué tenemos.

root@cks-master:~/app# docker build -t myapp .
...
---> Running in fd1ca9051d2c
Removing intermediate container fd1ca9051d2c
---> a16027a62703
Step 6/6 : CMD ["./app"]
---> Running in b41a8913eb93
Removing intermediate container b41a8913eb93
---> f6cb7eeab29a
Successfully built f6cb7eeab29a
Successfully tagged myapp:latest

# ¿Una imagen de 667MB para ejecutar esto?
root@cks-master:~/app# docker image ls myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp latest f6cb7eeab29a 20 minutes ago 667MB

# Esta sería la imagen base que estamos usando antes de añadir nuestras capas.
root@cks-master:~/app# docker image ls ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest edbfe74c41f8 4 weeks ago 78.1MB


root@cks-master:~/app# docker run myapp
user: root id: 0
user: root id: 0
user: root id: 0
user: root id: 0

Analizando el Dockerfile que tenemos estamos instalando Go para construir la aplicación. El binario generado por Go no necesita Go.

FROM ubuntu
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go
CMD ["./app"]

Lo que podemos hacer para reducir es utilizar solamente el binario generado por esta imagen en otra imagen mucho menor.

FROM ubuntu # from=0 es el stage 0
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go
##CMD ["./app"] # No vamos a ejecutar nada aquí

FROM alpine # from=1 es el stage 1
COPY --from=0 /app . # from=0 es igual al primer from
CMD ["./app"]

Si queremos dar nombre a los stages podemos.

FROM ubuntu AS builder
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go . # Está copiando para /
RUN CGO_ENABLED=0 go build app.go # generó el binario app en /

FROM alpine
COPY --from=builder /app . # Estamos copiando
CMD ["./app"]

Solo para contextualizar lo que dije sobre dónde los binarios fueron generados.

root@cks-master:~/app# docker run -d myapp
8d9643b4a460a90ca42bf747785675f8f24fb1db93b4c7f43b453388d3fa6e3f
root@cks-master:~/app# docker exec -it 8d9643b4a460a90ca42bf747785675f8f24fb1db93b4c7f43b453388d3fa6e3f bash
root@8d9643b4a460:/# ls
app app.go bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
# mira el app y el app.go allí

Ahora vamos a construir usando este Dockerfile multietapa.

root@cks-master:~/app# cat Dockerfile
FROM ubuntu AS builder
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine
COPY --from=builder /app .
CMD ["./app"]
root@cks-master:~/app# docker build -t myapp2 .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/

Sending build context to Docker daemon 3.072kB
Step 1/8 : FROM ubuntu AS builder
---> edbfe74c41f8
Step 2/8 : ARG DEBIAN_FRONTEND=noninteractive
---> Using cache
---> cc17dfd2ebce
Step 3/8 : RUN apt-get update && apt-get install -y golang-go
---> Using cache
---> a7ba366efacb
Step 4/8 : COPY app.go .
---> Using cache
---> 3456ce02a619
Step 5/8 : RUN CGO_ENABLED=0 go build app.go
---> Using cache
---> a16027a62703
Step 6/8 : FROM alpine
latest: Pulling from library/alpine
c6a83fedfae6: Already exists
Digest: sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5
Status: Downloaded newer image for alpine:latest
---> 324bc02ae123
Step 7/8 : COPY --from=builder /app .
---> df3957b29a0a
Step 8/8 : CMD ["./app"]
---> Running in 6082a17dad37
Removing intermediate container 6082a17dad37
---> df7ab2f5df67
Successfully built df7ab2f5df67
Successfully tagged myapp2:latest

# la diferencia de tamaño es brutal
root@cks-master:~/app# docker image ls | grep myapp
myapp2 latest df7ab2f5df67 40 seconds ago 9.82MB
myapp latest f6cb7eeab29a 41 minutes ago 667MB

root@cks-master:~/app# docker run myapp2
user: root id: 0
user: root id: 0
user: root id: 0

Alpine es una distribución muy pequeña solamente con lo necesario instalado siendo ideal para ejecutar contenedores reduciendo mucho la superficie de ataque además de disminuir los recursos usados y aumentar la velocidad de inicialización.

Pero tenemos más cosas para mejorar a nivel de seguridad:

  1. Estamos ejecutando como root y no lo necesitamos, nuestra aplicación no requiere esto.
  2. No fijamos ninguna tag para alpine y para ubuntu, o sea, estamos siempre ejecutando en la tag latest que siempre está siendo sustituida con nuevas mejoras que pueden contener issues aún no detectados. Otro punto importante es mantener esas capas en caché garantizando una mejor velocidad en la creación de las imágenes en un pipeline por ejemplo. Otro punto crucial es que podemos utilizar herramientas de terceros para mapear CVEs en esas tags específicas y solamente entonces vamos a necesitar alterar esto. Este es el flujo correcto desde el punto de vista de la seguridad.
  3. Remover permisos de escritura en el sistema de archivos. Podemos hacer esto en kubernetes con el security context, pero garantizar esto en la imagen anticipadamente es una redundancia bienvenida. Identifica directorios que no necesitan ser escritos y remueve el permiso de escritura.
  4. Remover acceso a los shells. Tener un shell en el contenedor muchas veces no es necesario y solo beneficiará a un invasor. Es un método más de disminuir la superficie de ataque.

Teniendo esto en vista ¿qué podemos hacer?

root@cks-master:~/app# vim Dockerfile

root@cks-master:~/app# cat Dockerfile
FROM ubuntu:24.04 AS builder
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

# Tags fijadas
FROM alpine:3.20.2
# Removido permiso de escritura en este directorio
RUN chmod a-w /etc
# Creando un usuario para no utilizar el root necesita ser hecho antes del comando abajo sino no tendríamos esos comandos disponibles
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
# Removiendo los shells y otros bin
RUN rm -rf /bin/*
# Copiando ahora directo para la carpeta del usuario
COPY --from=builder /app /home/appuser
# Alterando el usuario y directorio
USER appuser
WORKDIR /home/appuser
CMD ["./app"]

root@cks-master:~/app# docker build -t myappfinal .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/

Sending build context to Docker daemon 3.072kB
Step 1/13 : FROM ubuntu:24.04 AS builder
---> edbfe74c41f8
Step 2/13 : ARG DEBIAN_FRONTEND=noninteractive
---> Using cache
---> cc17dfd2ebce
Step 3/13 : RUN apt-get update && apt-get install -y golang-go
---> Using cache
---> a7ba366efacb
Step 4/13 : COPY app.go .
---> Using cache
---> 3456ce02a619
Step 5/13 : RUN CGO_ENABLED=0 go build app.go
---> Using cache
---> a16027a62703
Step 6/13 : FROM alpine:3.20.2
---> 324bc02ae123
Step 7/13 : RUN chmod a-w /etc
---> Running in 8a24da54d744
Removing intermediate container 8a24da54d744
---> 604937bb81e5
Step 8/13 : RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
---> Running in c4ccb98c16d3
Removing intermediate container c4ccb98c16d3
---> fd90a03bb0b2
Step 9/13 : RUN rm -rf /bin/*
---> Running in f6176856d71f
Removing intermediate container f6176856d71f
---> 68d85c673d6f
Step 10/13 : COPY --from=builder /app /home/appuser
---> c6bb7b7bb63f
Step 11/13 : USER appuser
---> Running in ac35ed24aa9c
Removing intermediate container ac35ed24aa9c
---> d860506721f3
Step 12/13 : WORKDIR /home/appuser
---> Running in 95b18e9db7e1
Removing intermediate container 95b18e9db7e1
---> 26d6d41d0de0
Step 13/13 : CMD ["./app"]
---> Running in 3bf244665fbd
Removing intermediate container 3bf244665fbd
---> 3cf77037f319
Successfully built 3cf77037f319
Successfully tagged myappfinal:latest
root@cks-master:~/app# docker run myappfinal
user: appuser id: 100
user: appuser id: 100

Extra

Vamos a pensar en una imagen ubuntu instalando cosas.

FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive

# Layer 1
RUN apt-get update && \
apt-get install -y \
golang-go \
python3 \
curl \
vim

# Layer 2
RUN apt-get clean && rm -rf /var/list/apt/lists/*

# Layer 3
# Removido permiso de escritura en este directorio
RUN chmod a-w /etc

# Layer 4
# Creando un usuario para no utilizar el root necesita ser hecho antes del comando abajo sino no tendríamos esos comandos disponibles
RUN groupadd appgroup && useradd -g appgroup -d /home/appuser -m appuser

# Alterando el usuario y directorio
USER appuser
WORKDIR /home/appuser