Security Images
Faça um breve review em cka container and images.
Uma coisa bem importante é entender que contêineres são um grupo de camadas sendo que somente a última camada do topo é escrevível. As camadas anteriores são somente leitura.
A maioria das vezes criamos uma imagem a partir de outra imagem como base. Estamos importando todas as camadas que ela possui e adicionando novas camadas.
Nem todo comando no Dockerfile cria uma camada, somente os comandos RUN, COPY e ADD. Toda vez que colocamos uma nova camada estão aumentando o tamanho da imagem.
FROM ubuntu # IMPORTANDO AS CAMADAS QUE EXISTE NESTA IMAGEM
RUN apt-get update & apt-get install -y golang-go # ADICIONANDO ESTA CAMADA
CMD ["sh"]
Como reduzir o tamanho de uma imagem?
Vamos imaginar que estamos querendo instalar o curl e usando a imagem do nginx como base.
O que temos é isso..

Vamos partir do seguinte Dockerfile e do app.go que seria uma possível aplicação.
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#
É um app simples com um loop infinito imprimindo user, id e userid.
Se fizermos o build disso e rodar olha o tamanho da imagem, vamos rodar e ver o que temos.
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
# Uma imagem de 667MB pra rodar isso?
root@cks-master:~/app# docker image ls myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp latest f6cb7eeab29a 20 minutes ago 667MB
# Essa seria a imagem base que estamos usando antes de adicionar nossas camadas.
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
Analisando o Dockerfile que temos estamos instalando o go para construir a aplicação. O binário gerado pelo Go não precisa do 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"]
O que podemos fazer para reduzir é utilizar somente o binário gerado por esta imagem em outra imagem muito menor.
FROM ubuntu # from=0 é o 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"] # Não vamos executar nada aqui
FROM alpine # from=1 é o stage 1
COPY --from=0 /app . # from=0 é igual primeiro from
CMD ["./app"]
Se quisermos dar nome aos stages podemos.
FROM ubuntu AS builder
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go . # Esta copiando para o /
RUN CGO_ENABLED=0 go build app.go # gerou o binário app no /
FROM alpine
COPY --from=builder /app . # Estamos copiando
CMD ["./app"]
Só para contextualizar o que eu disse sobre onde os binários foram gerados.
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
# olha o app e o app.go ali
Agora vamos construir usando este Dockerfile multi stage.
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
# a diferença de tamanho é 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 é uma distribuição muito pequena somente com o necessário instalado sendo ideal para rodar contêineres reduzindo muito a superfície de ataque além de diminuir os recursos usados e aumentar a velocidade de inicialização.
Mas temos mais coisas para melhorar em nível de segurança
- Estamos rodando como root e não precisamos, nossa aplicação não necessita disso.
- Não fixamos nenhuma tag para o alpine e para o ubuntu, ou seja, estamos sempre rodando na tag latest que é sempre esta sendo substituída com novas melhorias que podem conter issues ainda não detectadas. Outro ponto importante é manter essas layer em cache garantindo uma melhor velocidade na criação das imagens em um pipeline por exemplo. Outro ponto crucial é que podemos utilizar ferramentas de terceiros para mapear CVEs nessas tags específicas e somente então vamos precisar alterar isso. Esse é o workflow correto no ponto de vista da segurança.
- Remover permissões de escrita no filesystem. Podemos fazer isso no kubernetes com o security context, mas garantir isso na imagem antecipadamente é uma redundância bem vinda. Identifique diretório que não precisam ser escritos e remova as a permissão de escrita.
- Remover acesso aos shells. Ter um shell no contêiner muitas vezes não é necessário e só irá beneficiar um invasor. É mais um método de diminuir a superfície de ataque.
Tendo isso em vista o que podemos fazer?
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 Fixadas
FROM alpine:3.20.2
# Removida permissão de escrita nesse diretório
RUN chmod a-w /etc
# Criando um usuário para não utilizar o root precisa ser feito antes do comando abaixo senão não teríamos esses comandos disponíveis
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
# Removendo os shells e outros bin
RUN rm -rf /bin/*
# Copiando agora direto para pasta do usuário
COPY --from=builder /app /home/appuser
# Alterando o usuário e diretório
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 pensar em uma imagem ubuntu instalando coisas.
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
# Removida permissão de escrita nesse diretório
RUN chmod a-w /etc
# Layer 4
# Criando um usuário para não utilizar o root precisa ser feito antes do comando abaixo senão não teríamos esses comandos disponíveis
RUN groupadd appgroup && useradd -g appgroup -d /home/appuser -m appuser
# Alterando o usuário e diretório
USER appuser
WORKDIR /home/appuser