Offline/air-gapped deployments: clusters sem internet

1. Introdução aos Ambientes Air-gapped

Ambientes air-gapped, ou isolados, são clusters Kubernetes completamente desconectados da internet pública. Eles são comuns em indústrias como defesa, governo, finanças, energia e edge computing, onde dados sensíveis ou operações críticas exigem isolamento total.

Os desafios principais incluem: ausência de acesso a repositórios públicos (Docker Hub, GitHub, registry oficial do Kubernetes), necessidade de atualizações manuais e sincronização cuidadosa de imagens e pacotes. Diferente de clusters conectados, onde kubectl apply ou helm install puxam recursos automaticamente, em ambientes offline cada componente precisa ser pré-carregado.

A complexidade operacional é maior, mas os ganhos em segurança — eliminação de ataques de supply chain, vazamento de dados e dependência de terceiros — justificam o esforço.

2. Planejamento da Infraestrutura Offline

Antes de qualquer deploy, é essencial inventariar todas as dependências:

  • Imagens Docker de todos os componentes do cluster (etcd, kube-apiserver, kube-controller-manager, kube-scheduler, coredns, kube-proxy)
  • Imagens das aplicações que rodarão no cluster
  • Charts Helm e suas dependências
  • Pacotes de sistema (containerd, runc, kubernetes-cni, kubectl)
  • Binários do Kubernetes (kubeadm, kubelet)

Estratégias de mirroring incluem:

  • Registry privado: Harbor, Nexus, Docker Registry com autenticação
  • Proxies de pacotes: Artifactory, Pulp para APT/YUM
  • Versionamento rigoroso com GitOps (ArgoCD em modo offline) para evitar divergências

3. Preparação do Registry Privado e Sincronização de Imagens

Vamos configurar um registry local simples com Docker Registry e autenticação básica.

# No ambiente conectado, crie o arquivo de configuração
mkdir -p /data/registry
cat > /data/registry/config.yml <<EOF
version: 0.1
log:
  level: info
storage:
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
auth:
  htpasswd:
    realm: basic-realm
    path: /etc/docker/registry/htpasswd
EOF

# Gere senha (usuário: admin, senha: admin123)
docker run --entrypoint htpasswd httpd:2 -Bbn admin admin123 > /data/registry/htpasswd

# Inicie o registry
docker run -d -p 5000:5000 --name registry \
  -v /data/registry:/etc/docker/registry \
  registry:2

Para sincronizar imagens, use skopeo:

# Baixar imagem do Docker Hub
skopeo copy docker://docker.io/library/nginx:1.25 \
  docker://localhost:5000/library/nginx:1.25

# Sincronizar múltiplas imagens com script
for img in kube-apiserver:v1.28.0 kube-controller-manager:v1.28.0 \
           kube-scheduler:v1.28.0 kube-proxy:v1.28.0 coredns:v1.10.0 \
           etcd:3.5.9 pause:3.9; do
  skopeo copy docker://registry.k8s.io/$img \
    docker://localhost:5000/k8s/$img
done

Para sincronização incremental, use crane:

crane copy library/nginx:1.25 localhost:5000/library/nginx:1.25

4. Deploy do Cluster Kubernetes sem Internet

No ambiente air-gapped, instale o Kubernetes usando kubeadm com imagens pré-carregadas.

# No nó mestre, carregue as imagens localmente
for img in kube-apiserver:v1.28.0 kube-controller-manager:v1.28.0 \
           kube-scheduler:v1.28.0 kube-proxy:v1.28.0 coredns:v1.10.0 \
           etcd:3.5.9 pause:3.9; do
  ctr image pull localhost:5000/k8s/$img
  ctr image tag localhost:5000/k8s/$img registry.k8s.io/$img
done

# Configure containerd para usar mirror do registry local
cat > /etc/containerd/config.toml <<EOF
version = 2
[plugins."io.containerd.grpc.v1.cri".registry]
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
      endpoint = ["http://localhost:5000"]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]
      endpoint = ["http://localhost:5000"]
EOF

systemctl restart containerd

# Inicialize o cluster
kubeadm init --pod-network-cidr=10.244.0.0/16 \
  --image-repository=localhost:5000/k8s

Para instalação sem pacotes APT/YUM, use binários estáticos:

# Baixe os binários em máquina conectada
wget https://dl.k8s.io/v1.28.0/kubernetes-node-linux-amd64.tar.gz
# Transfira para o ambiente offline e extraia
tar -xzf kubernetes-node-linux-amd64.tar.gz
cp kubernetes/node/bin/kubelet /usr/local/bin/
cp kubernetes/node/bin/kubectl /usr/local/bin/

5. Gerenciamento de Aplicações e Helm Charts em Modo Offline

Crie um repositório Helm local com ChartMuseum:

# No ambiente conectado, instale ChartMuseum
docker run -d -p 8080:8080 --name chartmuseum \
  -e STORAGE=local \
  -e STORAGE_LOCAL_ROOTDIR=/charts \
  -v /data/charts:/charts \
  chartmuseum/chartmuseum:latest

# Adicione charts ao repositório
helm repo add stable https://charts.helm.sh/stable
helm fetch stable/nginx-ingress --version 1.41.3
curl --data-binary "@nginx-ingress-1.41.3.tgz" \
  http://localhost:8080/api/charts

# No ambiente offline, use o repositório local
helm repo add local http://registry-interno:8080
helm install my-nginx local/nginx-ingress --version 1.41.3

Para versionamento de dependências:

# Em máquina conectada
helm dependency build ./my-chart
helm package ./my-chart
# Transfira o .tgz para o ambiente offline

6. Segurança e Compliance em Ambientes Isolados

Configure OPA/Gatekeeper para garantir que apenas imagens do registry local sejam usadas:

# ConstraintTemplate
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not startswith(container.image, "registry-interno:5000/")
          msg := sprintf("Imagem %v não permitida. Use apenas registry-interno:5000", [container.image])
        }

# Constraint
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: repo-is-required
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]

Assine imagens com Cosign sem acesso externo:

# Gere chave localmente
cosign generate-key-pair

# Assine imagem
cosign sign --key cosign.key registry-interno:5000/minha-app:v1.0

# Verifique
cosign verify --key cosign.pub registry-interno:5000/minha-app:v1.0

7. Manutenção e Atualizações Contínuas

Para atualizar o cluster offline:

# Backup do etcd
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db

# Baixe novas imagens em máquina conectada
skopeo copy docker://registry.k8s.io/kube-apiserver:v1.29.0 \
  docker://localhost:5000/k8s/kube-apiserver:v1.29.0

# Transfira para ambiente offline
# No cluster, atualize
kubeadm upgrade plan
kubeadm upgrade apply v1.29.0

Para sincronização de vulnerabilidades, use Trivy offline:

# Baixe banco de CVEs em máquina conectada
trivy image --download-db-only --cache-dir /data/trivy-db

# Transfira para ambiente offline
# Escaneie imagens
trivy image --cache-dir /data/trivy-db \
  registry-interno:5000/minha-app:v1.0

8. Exemplo Prático: Pipeline Completo de Deploy Offline

# === FASE 1: PREPARAÇÃO (Máquina conectada) ===
# 1. Baixar imagens
./sync-images.sh

# 2. Baixar charts Helm
helm repo add bitnami https://charts.bitnami.com/bitnami
helm fetch bitnami/nginx --version 15.0.0
helm fetch bitnami/postgresql --version 12.5.0

# 3. Baixar binários
wget https://dl.k8s.io/v1.28.0/kubernetes-node-linux-amd64.tar.gz

# === FASE 2: TRANSFERÊNCIA ===
# Copiar para USB/mídia física:
# - /data/registry (imagens)
# - /charts/*.tgz
# - kubernetes-node-linux-amd64.tar.gz

# === FASE 3: DEPLOY (Ambiente offline) ===
# 1. Iniciar registry
docker load < registry.tar
docker run -d -p 5000:5000 --name registry registry:2

# 2. Carregar imagens
for img in $(cat images.txt); do
  docker load < $img.tar
  docker tag $img localhost:5000/$img
  docker push localhost:5000/$img
done

# 3. Instalar Kubernetes
tar -xzf kubernetes-node-linux-amd64.tar.gz
cp kubernetes/node/bin/kubelet /usr/local/bin/
kubeadm init --image-repository=localhost:5000/k8s

# 4. Deploy aplicação com Helm
helm repo add local http://localhost:8080
helm install my-app local/nginx --version 15.0.0

# 5. Validação
kubectl get pods -A
kubectl logs -n default my-app-nginx-xxxx
kubectl describe pod my-app-nginx-xxxx

Referências