Pular para o conteúdo principal

Admission Webhook for K8s

1. Visão Geral & Documentos para Casbin K8s-Gatekeeper

Casbin K8s-GateKeeper é um webhook de admissão do Kubernetes que integra o Casbin como ferramenta de Controle de Acesso. Ao usar o Casbin K8s-GateKeeper, você pode estabelecer regras flexíveis para autorizar ou interceptar qualquer operação em recursos do K8s, SEM escrever nenhum código, mas apenas várias linhas de configurações declarativas de modelos e políticas do Casbin, que fazem parte da linguagem ACL (Lista de Controle de Acesso) do Casbin.

Casbin K8s-GateKeeper é desenvolvido e mantido pela comunidade Casbin. O repositório deste projeto está disponível aqui: https://github.com/casbin/k8s-gatekeeper

0.1 Um Exemplo Simples

Por exemplo, você não precisa escrever nenhum código, mas usar as seguintes linhas de configuração para alcançar esta função: 'Proibir imagens com algumas tags especificadas de serem usadas em quaisquer implantações':

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(${OBJECT}.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

And Policy:

p, "1.14.1",deny

Estas estão na linguagem comum de ACL do Casbin. Suponha que você já tenha lido capítulos sobre eles, será muito fácil de entender.

Casbin K8s-Gatekeeper tem as seguintes vantagens:

  • Fácil de usar. Escrever várias linhas de ACL é muito melhor do que escrever muitos códigos.
  • Permite atualizações quentes de configurações. Você não precisa desligar todo o plugin para modificar configurações.
  • É flexível. Regras arbitrárias podem ser feitas em qualquer recurso do K8s, que podem ser exploradas com kubectl gatekeeper.
  • Simplifica a implementação do webhook de admissão do K8s, que é muito complicado. Você não precisa saber o que é um webhook de admissão do K8s ou como escrever código para ele. Tudo o que você precisa fazer é conhecer o recurso no qual deseja impor restrições e depois escrever ACL do Casbin. Todos sabem que o K8s é complexo, mas ao usar o Casbin K8s-Gatekeeper, seu tempo pode ser economizado.
  • É mantido pela comunidade Casbin. Sinta-se à vontade para nos contatar se algo sobre este plugin o confundir ou se você encontrar algum problema ao tentar isso.

1.1 Como o Casbin K8s-Gatekeeper Funciona?

K8s-Gatekeeper é um webhook de admissão para K8s que usa Casbin para aplicar regras de controle de acesso definidas pelo usuário de forma arbitrária para ajudar a prevenir qualquer operação no K8s que o administrador não deseje.

Casbin é uma biblioteca de controle de acesso de código aberto poderosa e eficiente. Ela oferece suporte para a aplicação de autorização baseada em vários modelos de controle de acesso. Para mais detalhes sobre o Casbin, veja Visão Geral.

Webhooks de admissão no K8s são callbacks HTTP que recebem 'solicitações de admissão' e fazem algo com elas. Em particular, K8s-Gatekeeper é um tipo especial de webhook de admissão: 'ValidatingAdmissionWebhook', que pode decidir se aceita ou rejeita esta solicitação de admissão ou não. Quanto às solicitações de admissão, são solicitações HTTP descrevendo uma operação em recursos especificados do K8s (por exemplo, criar/deletar uma implantação). Para mais sobre webhooks de admissão, veja documentação oficial do K8s.

1.2 Um Exemplo Ilustrando Como Funciona

Por exemplo, quando alguém quer criar uma implantação contendo um pod executando nginx (usando kubectl ou clientes K8s), o K8s gerará uma solicitação de admissão, que (se traduzida para o formato YAML) pode ser algo assim:

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.1
ports:
- containerPort: 80

Esta solicitação passará pelo processo de todos os middlewares mostrados na imagem, incluindo nosso K8s-Gatekeeper. K8s-Gatekeeper pode detectar todos os aplicadores de regras do Casbin armazenados no etcd do K8s, que é criado e mantido pelo usuário (via kubectl ou o cliente Go que fornecemos). Cada aplicador contém um modelo Casbin e uma política Casbin. A solicitação de admissão será processada por cada aplicador, um por um, e apenas passando por todos os aplicadores uma solicitação pode ser aceita por este K8s-Gatekeeper.

(Se você não entende o que é um aplicador, modelo ou política do Casbin, veja este documento: Começar).

Por exemplo, por algum motivo, o administrador quer proibir a aparição da imagem 'nginx:1.14.1' enquanto permite 'nginx:1.3.1'. Um aplicador contendo a seguinte regra e política pode ser criado (Explicaremos como criar um aplicador, o que são esses modelos e políticas, e como escrevê-los nos capítulos seguintes).

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

Policy:

p, "nginx:1.13.1",allow
p, "nginx:1.14.1",deny

Ao criar um aplicador contendo o modelo e a política acima, a solicitação de admissão anterior será rejeitada por este aplicador, o que significa que o K8s não criará esta implantação.

2 Instalar K8s-gatekeeper

Há três métodos disponíveis para instalar K8s-gatekeeper: Webhook externo, Webhook interno e Helm.

nota

Observação: Esses métodos são apenas para usuários experimentarem o K8s-gatekeeper e não são seguros. Se você deseja usá-lo em um ambiente produtivo, por favor, certifique-se de ler Capítulo 5. Configurações avançadas e fazer as modificações necessárias antes da instalação. 2.1 Webhook interno

2.1.1 Passo 1: Construir a imagem

2.1.1 Step 1: Build the image

Para o método interno de webhook, o próprio webhook será implementado como um serviço dentro do Kubernetes. Para criar o serviço e a implantação necessários, você precisa construir uma imagem do K8s-gatekeeper. Você pode construir sua própria imagem executando o seguinte comando:

docker build --target webhook -t k8s-gatekeeper .

Este comando criará uma imagem local chamada 'k8s-gatekeeper:latest'.

nota

Nota: Se você estiver usando minikube, por favor execute eval $(minikube -p minikube docker-env) antes de rodar 'docker build'.

2.1.2 Passo 2: Configurar serviços e implantações para o K8s-gatekeeper

Execute os seguintes comandos:

kubectl apply -f config/rbac.yaml
kubectl apply -f config/webhook_deployment.yaml
kubectl apply -f config/webhook_internal.yaml

Isso iniciará a execução do K8s-gatekeeper, e você pode confirmar isso executando kubectl get pods.

2.1.3 Passo 3: Instalar Recursos CRD para o K8s-gatekeeper

Execute os seguintes comandos:

kubectl apply -f config/auth.casbin.org_casbinmodels.yaml 
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml

2.2 Webhook externo

Para o método de webhook externo, o K8s-gatekeeper estará rodando fora do Kubernetes, e o Kubernetes acessará o K8s-gatekeeper como acessaria um site comum. O Kubernetes tem um requisito obrigatório de que o webhook de admissão deve ser HTTPS. Para o propósito de testar o K8s-gatekeeper, fornecemos um conjunto de certificados e uma chave privada (embora isso não seja seguro). Se você preferir usar seu próprio certificado, por favor consulte Capítulo 5. Configurações avançadas para instruções sobre como ajustar o certificado e a chave privada. O certificado que fornecemos é emitido para 'webhook.domain.local'.

Então, modifique o host (por exemplo, /etc/hosts) e aponte 'webhook.domain.local' para o endereço IP no qual o K8s-gatekeeper está rodando. Em seguida, execute o seguinte comando:

2.3 Instalar o K8s-gatekeeper via Helm

go mod tidy
go mod vendor
go run cmd/webhook/main.go
kubectl apply -f config/auth.casbin.org_casbinmodels.yaml
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml
kubectl apply -f config/webhook_external.yaml

2.3.1 Passo 1: Construir a imagem

Por favor, consulte Capítulo 2.1.1.

2.3.2 Instalação via Helm

Execute o comando helm install k8sgatekeeper ./k8sgatekeeper.

Experimente o K8s-gatekeeper 3.1 Criar Modelo e Política Casbin

Você tem dois métodos para criar um modelo e política: via kubectl ou via o cliente go que fornecemos.

3.1.1 Criar/Atualizar Modelo e Política Casbin via kubectl

No K8s-gatekeeper, o Modelo Casbin é armazenado em um recurso CRD chamado 'CasbinModel'.

Sua definição está localizada em config/auth.casbin.org_casbinmodels.yaml. Existem exemplos em example/allowed_repo/model.yaml.

Preste atenção nos seguintes campos: metadata.name: o nome do modelo.

  • Este nome DEVE ser o mesmo que o nome do objeto CasbinPolicy relacionado a este modelo, para que o K8s-gatekeeper possa emparelhá-los e criar um executor. spec.enable: se este campo estiver definido como 'false', este modelo (bem como o objeto CasbinPolicy relacionado a este modelo) será ignorado.
  • spec.modelText: uma string que contém o texto do modelo de um Modelo Casbin.
  • A Política Casbin é armazenada em outro recurso CRD chamado 'CasbinPolicy', cuja definição pode ser encontrada em config/auth.casbin.org_casbinpolicies.yaml.

Existem exemplos em example/allowed_repo/policy.yaml.

Preste atenção nos seguintes campos: metadata.name: o nome da política.

  • Este nome DEVE ser o mesmo que o nome do objeto CasbinModel relacionado a esta política, para que o K8s-gatekeeper possa emparelhá-los e criar um executor. spec.policyItem: uma string que contém o texto da política de um Modelo Casbin.
  • Após criar seus próprios arquivos CasbinModel e CasbinPolicy, use o seguinte comando para aplicá-los:

Uma vez que um par de CasbinModel e CasbinPolicy é criado, o K8s-gatekeeper será capaz de detectá-lo dentro de 5 segundos.

kubectl apply -f <filename>

3.1.2 Criar/Atualizar Modelo e Política Casbin via o cliente go que fornecemos

Entendemos que pode haver situações em que não é conveniente usar o shell para executar comandos diretamente em um nó do cluster K8s, como quando você está construindo uma plataforma de nuvem automática para sua corporação.

Portanto, desenvolvemos um cliente go para criar e manter CasbinModel e CasbinPolicy. A biblioteca do cliente go está localizada em pkg/client.

Em client.go, fornecemos uma função para criar um cliente.

In client.go, we provide a function to create a client.

func NewK8sGateKeeperClient(externalClient bool) (*K8sGateKeeperClient, error) 

O parâmetro externalClient determina se o K8s-gatekeeper está executando dentro do cluster K8s ou não.

Em model.go, fornecemos várias funções para criar, deletar e modificar CasbinModel. Você pode descobrir como usar essas interfaces em model_test.go.

Em policy.go, fornecemos várias funções para criar, deletar e modificar CasbiPolicy. Você pode descobrir como usar essas interfaces em policy_test.go.

3.1.2 Tente se o K8s-gatekeeper funciona

Suponha que você já tenha criado o modelo e a política exatos em example/allowed_repo. Agora, tente o seguinte comando:

kubectl apply -f example/allowed_repo/testcase/reject_1.yaml

Você deve descobrir que o K8s rejeitará esta solicitação e mencionará que o webhook foi a razão pela qual esta solicitação foi rejeitada. No entanto, quando você tentar aplicar example/allowed_repo/testcase/approve_2.yaml, ele será aceito.

4. Como Escrever Modelo e Política com K8s-gatekeeper

Antes de tudo, certifique-se de que você está familiarizado com a gramática básica dos Modelos e Políticas Casbin. Se você não está, por favor, leia a seção Get Started primeiro. Neste capítulo, assumimos que você já entende o que são Modelos e Políticas Casbin.

4.1 Definição de Solicitação de Modelo

Quando o K8s-gatekeeper está autorizando uma solicitação, a entrada é sempre um objeto: o objeto Go da Solicitação de Admissão. Isso significa que o executor sempre será usado assim:

ok, err := enforcer.Enforce(admission)

onde admission é um objeto AdmissionReview definido pela api oficial do K8s "k8s.io/api/admission/v1". Você pode encontrar a definição desta estrutura neste repositório: https://github.com/kubernetes/api/blob/master/admission/v1/types.go. Para mais informações, você também pode consultar https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response.

Portanto, para qualquer modelo usado pelo K8s-gatekeeper, a definição do request_definition deve ser sempre assim:

    [request_definition]
r = obj

O nome 'obj' não é obrigatório, desde que o nome seja consistente com o nome usado na parte [matchers].

4.2 Correspondentes de Modelo

Você deve usar o recurso ABAC do Casbin para escrever suas regras. No entanto, o avaliador de expressão integrado no Casbin não suporta indexação em mapas ou arrays (fatias), nem a expansão de arrays. Portanto, o K8s-gatekeeper fornece várias 'funções Casbin' como extensões para implementar esses recursos. Se você ainda achar que sua demanda não pode ser atendida por essas extensões, sinta-se à vontade para iniciar um problema ou criar um pull request.

Se você não está familiarizado com as funções Casbin, você pode consultar Function para mais informações.

Aqui estão as funções de extensão:

4.2.1 Funções de extensão

4.2.1.1 access

Access é usado para resolver o problema de que o Casbin não suporta indexação em mapas ou arrays. O exemplo example/allowed_repo/model.yaml demonstra o uso desta função:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

Neste correspondente, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") é equivalente a r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Image, onde r.obj.Request.Object.Object.Spec.Template.Spec.Containers é uma fatia.

Access também pode chamar funções simples que não têm parâmetros e retornam um único valor. O exemplo example/container_resource_limit/model.yaml demonstra isso:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value")) >= parseFloat(p.cpu) && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","memory","Value")) >= parseFloat(p.memory)

Neste correspondente, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value") é equivalente a r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value(), onde r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits é um mapa, e Value() é uma função simples que não tem parâmetros e retorna um único valor.

4.2.1.2 accessWithWildcard

Às vezes, você pode ter uma demanda como esta: todos os elementos de um array devem ter um prefixo "aaa". No entanto, o Casbin não suporta loops for. Com accessWithWildcard e o recurso de "expansão de map/slice", você pode implementar facilmente tal demanda.

Por exemplo, suponha que a.b.c seja um array [aaa,bbb,ccc,ddd,eee], então o resultado de accessWithWildcard(a,"b","c","*") será uma fatia [aaa,bbb,ccc,ddd,eee]. Usando o curinga *, a fatia é expandida.

Da mesma forma, o curinga pode ser usado mais de uma vez. Por exemplo, o resultado de accessWithWildcard(a,"b","c","*","*") será [a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...].

4.2.1.3 Funções que Suportam Argumentos de Comprimento Variável

No avaliador de expressão do Casbin, quando um parâmetro é um array, ele será automaticamente expandido como um argumento de comprimento variável. Utilizando esse recurso para suportar a expansão de array/fatia/mapa, também integramos várias funções que aceitam um array/fatia como parâmetro:

  • contain(): aceita vários parâmetros e retorna se algum parâmetro (exceto o último parâmetro) é igual ao último parâmetro.
  • split(a,b,c...,sep,index): retorna um slice que contém [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...].
  • len(): retorna o comprimento do argumento de comprimento variável.
  • matchRegex(a,b,c...,regex): retorna se todos os parâmetros dados (a, b, c, ...) correspondem ao regex fornecido.

Aqui está um exemplo em example/disallowed_tag/model.yaml:

    [matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

Assumindo que accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image") retorna ['a:b', 'c:d', 'e:f', 'g:h'], porque splits suporta argumentos de comprimento variável e realiza a operação de splits em cada elemento, o elemento no índice 1 será selecionado e retornado. Portanto, split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) retorna ['b','d','f','h']. E contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj) retorna se p.obj está contido em ['b','d','f','h'].

4.2.1.2 Funções de Conversão de Tipo

  • ParseFloat(): Converte um inteiro para um float (isso é necessário porque qualquer número usado na comparação deve ser convertido em um float).
  • ToString(): Converte um objeto para uma string. Este objeto deve ter um tipo básico de string (por exemplo, um objeto do tipo XXX quando há uma declaração type XXX string).
  • IsNil(): Retorna se o parâmetro é nulo.

5. Configurações Avançadas

5.1 Sobre Certificados

No Kubernetes (k8s), é obrigatório que um webhook use HTTPS. Existem duas abordagens para alcançar isso:

  • Use certificados autoassinados (exemplos neste repositório usam este método)
  • Use um certificado normal

5.1.1 Certificados autoassinados

Usar um certificado autoassinado significa que a Autoridade Certificadora (CA) que emite o certificado não é uma das CAs bem conhecidas. Portanto, você deve informar o k8s sobre esta CA.

Atualmente, o exemplo neste repositório usa uma CA auto-criada, cuja chave privada e certificado estão armazenados em config/certificate/ca.crt e config/certificate/ca.key respectivamente. O certificado para o webhook é config/certificate/server.crt, que é emitido pela CA auto-criada. Os domínios deste certificado são "webhook.domain.local" (para webhook externo) e "casbin-webhook-svc.default.svc" (para webhook interno).

Informações sobre a CA são passadas para o k8s via arquivos de configuração do webhook. Tanto config/webhook_external.yaml quanto config/webhook_internal.yaml têm um campo chamado "CABundle", que contém uma string codificada em base64 do certificado da CA.

Caso você precise mudar o certificado/domínio (por exemplo, se você quiser colocar este webhook em outro namespace do k8s ao usar um webhook interno, ou se você quiser mudar o domínio ao usar um webhook externo), os seguintes procedimentos devem ser seguidos:

  1. Gere uma nova CA:

    • Gere a chave privada para a CA falsa:

      openssl genrsa -des3 -out ca.key 2048
    • Remova a proteção por senha da chave privada:

      openssl rsa -in ca.key -out ca.key
  2. Gere uma chave privada para o servidor webhook:

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. Use a CA auto-gerada para assinar o certificado para o webhook:

    • Copie o arquivo de configuração do openssl do seu sistema para uso temporário. Você pode descobrir a localização do arquivo de configuração executando openssl version -a, geralmente chamado openssl.cnf.

    • No arquivo de configuração:

      • Encontre o parágrafo [req] e adicione a seguinte linha: req_extensions = v3_req

      • Encontre o parágrafo [v3_req] e adicione a seguinte linha: subjectAltName = @alt_names

      • Acrescente as seguintes linhas ao arquivo:

        [alt_names]
        DNS.2=<The domain you want>

        Nota: Substitua 'casbin-webhook-svc.default.svc' pelo nome real do serviço do seu próprio serviço se você decidir modificar o nome do serviço.

    • Use o arquivo de configuração modificado para gerar um arquivo de solicitação de certificado:

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • Use a CA auto-criada para responder ao pedido e assinar o certificado:

      openssl x509 -req -days 3650 -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -extensions v3_req -extensions SAN -extfile openssl.cnf
  4. Substitua o campo 'CABundle': Tanto config/webhook_external.yaml quanto config/webhook_internal.yaml têm um campo chamado "CABundle", que contém uma string codificada em base64 do certificado da CA.

  5. Atualize este campo com o novo certificado.

Se você estiver usando helm, mudanças semelhantes precisam ser aplicadas aos gráficos do helm.

5.1.2 Certificados legais Se você usar certificados legais, não precisará passar por todos esses procedimentos.