Pular para o conteúdo
Página inicial da Latchkey
Comece grátis
Menu da documentação

Faça a sua nuvem confiar em um job do Latchkey

Como um job do Latchkey CI faz login na AWS com um token de identidade de vida curta em vez de uma chave de nuvem guardada: o emissor, o subject que a sua política de confiança compara e o passo que pede o token.

Resumo

  1. Um passo pede ao runner um token de identidade para um público, e o Latchkey o assina como https://oidc.latchkey.dev, então um job pode fazer deploy sem nenhuma chave de nuvem guardada em lugar nenhum.
  2. A sua política de confiança compara o token pelo seu sub, que nomeia o seu workspace, o repositório e a ref, ou o ambiente para o qual um owner ou admin aprovou a execução ou que definiu para não precisar de aprovação.
  3. Um token emitido para um job de uma execução de ensaio, ou de um pipeline em ensaio, traz o seu sub depois de advisory:, então uma role escrita para execuções reais nunca confia nele.
  4. Na AWS, crie um provedor OpenID Connect para https://oidc.latchkey.dev com o público sts.amazonaws.com e depois faça login com latchkey/aws-login@1.0.0.

Como funciona#

Um job de uma execução do Latchkey CI pode pedir um token de identidade: um token de vida curta, assinado pelo Latchkey, que diz a qual workspace, repositório e ref ou ambiente o job pertence. A sua nuvem verifica a assinatura com as chaves públicas que o Latchkey publica e compara as claims do token com a confiança que você escreveu, e quando elas batem entrega ao job credenciais de vida curta só dele. Nenhuma chave de nuvem fica guardada no Latchkey, em um segredo ou no seu arquivo de workflow.

Um passo pede um token para um público: o nome que a sua nuvem espera na claim aud do token, como sts.amazonaws.com para a AWS. Todas as outras claims vêm do registro do próprio Latchkey sobre o job, a sua execução e o seu repositório, nunca da requisição.

O emissor#

ItemValor
Emissorhttps://oidc.latchkey.dev
Documento de descobertahttps://oidc.latchkey.dev/.well-known/openid-configuration
Chaves públicashttps://oidc.latchkey.dev/.well-known/jwks.json
AssinaturaRS256, com a chave que o seu kid nomeia
Validade15 minutos, ou menos quando o timeout do job está mais perto, e nunca menos de 5 minutos

Escreva o emissor exatamente como acima, sem barra no final. Os dois documentos podem ficar em cache por uma hora. Quando o Latchkey troca a sua chave de assinatura, ele publica a chave nova ao lado da antiga antes de assinar com ela, então uma nuvem que lê as chaves públicas do emissor não precisa de nenhuma mudança do seu lado.

Quais jobs recebem um token#

Um job pode pedir enquanto roda, quando é um job de uma execução do Latchkey CI do seu arquivo de workflow sobre um commit do próprio repositório. Estes jobs não recebem token:

  • um job de um pull request de um fork, cujos passos não recebem o meio de pedir;
  • um job iniciado com latchkey run;
  • um job de uma execução que o Latchkey iniciou para verificar uma correção que o autorreparo propôs;
  • um job que nomeia um ambiente, até que um owner ou admin aprove a sua execução, a menos que o ambiente não precise de aprovação, e nunca em uma execução em uma branch que o ambiente não aceita.

Um token continua válido até o seu exp, no máximo 15 minutos, mesmo depois que o seu job termina. Nenhum token é emitido depois que o job termina.

O subject#

A claim sub é o que uma política de confiança compara. Ela tem uma de quatro formas:

Jobsub
Um job que não nomeia nenhum ambienteorg:<workspace id>:repo:<owner>/<name>:ref:<ref>
Um job que nomeia um ambiente, aprovado ou sem precisar de aprovaçãoorg:<workspace id>:repo:<owner>/<name>:environment:<environment>
A primeira, em uma execução de ensaio, em um pipeline em ensaio ou em modo consultivoadvisory:org:<workspace id>:repo:<owner>/<name>:ref:<ref>
A segunda, em uma execução de ensaio, em um pipeline em ensaio ou em modo consultivoadvisory:org:<workspace id>:repo:<owner>/<name>:environment:<environment>

<workspace id> é o id do seu workspace, o mesmo em todo token que os jobs do seu workspace recebem. Copie-o de Workspace ID em Settings, Account. Os tokens de todos os workspaces do Latchkey vêm do mesmo emissor, então uma política de confiança sempre nomeia o id do seu workspace no sub que compara. <owner>/<name> é o nome do repositório como o Latchkey o leu por último do seu host de código, então renomear o repositório muda os seus subjects: atualize as roles que o nomeiam quando o renomear, e remova-as quando o excluir, já que um repositório do seu workspace que pegar o nome depois recebe os mesmos subjects. <ref> a ref da execução à qual o job pertence, e <environment> o ambiente ao qual o job foi vinculado, como o arquivo de workflow o escreve, mantendo maiúsculas e minúsculas: o da própria perna para um valor da matrix, e o escolhido para um mapa de branches. Nenhuma parte pode conter um :, então nenhum subject pode ser lido como outro.

Para ver o subject exato que um job recebe, rode este passo uma vez nele. Ele imprime o sub decodificado e o id do workspace, nunca o token, que o log esconde de qualquer forma:

yaml
- name: Show this job's identity
  run: |
    answer=$(curl -sS --noproxy '*' -H "Authorization: Bearer $LATCHKEY_ID_TOKEN_REQUEST_TOKEN" \
      "$LATCHKEY_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
    if ! token=$(jq -er .value <<<"$answer" 2>/dev/null); then
      echo "No identity token: ${answer:-no answer}" >&2
      exit 1
    fi
    jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | @base64d | fromjson | .sub, .organization_id' <<<"$token"

Um repositório em modo consultivo roda os seus jobs como um ensaio ao lado do seu CI atual, e um repositório no Latchkey CI roda um pipeline em ensaio da mesma forma enquanto o arquivo do GitHub Actions que ele substitui ainda está no commit. Um token emitido para um job de uma execução de ensaio ou de um pipeline em ensaio traz o prefixo advisory:, e todo token emitido enquanto o repositório está em modo consultivo também traz, mesmo para uma execução que começou antes de o repositório passar para o modo consultivo. Todo subject sem advisory: começa com org:, então uma política de confiança escrita para execuções reais nunca aceita um token com o prefixo advisory:. Isso é intencional: nem uma execução de ensaio nem um pipeline em ensaio podem fazer deploy. Uma role pensada também para eles, como uma role somente leitura que um teste usa para ler, lista a forma advisory: ao lado da forma que começa com org:.

Um job que nomeia um ambiente recebe a forma de ambiente só depois que um owner ou admin aprova a sua execução para esse ambiente, ou sem aprovação quando um owner ou admin definiu o ambiente para não precisar de uma, e, quando um owner ou admin definiu as branches do ambiente em Settings, Environments, só em uma execução em uma delas. A sua claim ref continua nomeando a ref da execução. Uma role que confia em environment:production aceita, portanto, só jobs que um owner ou admin aprovou para production, ou que production iniciou sem aprovação, em execuções que production aceita. Um ambiente só pode dispensar a aprovação enquanto as suas branches estão definidas, e então essas branches são a sua única proteção. Até que um owner ou admin defina as branches, production aceita toda execução, em qualquer branch, tag ou ref de pull request, então defina-as antes que uma role de deploy confie no ambiente.

Uma aprovação também pode vir por uma chave de API com o escopo jobs:approve, em nome do owner ou admin que criou a chave. Um agente que tenha uma chave assim e também possa fazer push em um repositório pode aprovar as execuções das próprias mudanças, sem que nenhuma outra pessoa decida.

Qual ref um token nomeia#

Todo token de identidade nomeia a ref da execução à qual o seu job pertence, em duas claims:

  • ref, por exemplo refs/heads/main, refs/tags/v1.4.0 ou refs/pull/42/head;
  • sub, que termina em :ref:<ref> a menos que o job nomeie um ambiente.

Quando um push e um pull request nomeiam o mesmo commit, eles se juntam em uma execução, e todo job dessa execução traz a ref dessa execução.

Pull requests de uma branch do mesmo repositório#

Na maioria das vezes, os jobs de um pull request trazem a ref da branch, refs/heads/<branch>: você faz push da branch, o push inicia a execução do commit e o pull request se junta a essa execução. Abrir um pull request para um commit que já rodou não inicia nada novo; os checks dele são os do push.

Um token nomeia refs/pull/<number>/head quando o próprio pull request inicia a execução:

  • o seu arquivo de workflow roda em pull_request e não em push, então o push não corresponde a nada e o pull request inicia a execução; ou
  • a atualização do pull request chega ao Latchkey antes que o Latchkey tenha planejado os jobs do commit a partir do push.

Então um job em um pull request pode trazer qualquer uma das formas, dependendo dos gatilhos do seu workflow e de qual evento chega primeiro. A claim event_class nomeia o evento para o qual a execução foi planejada, como branch_push para um push ou pr_update para um pull request. Na execução de um pull request, a claim base_ref nomeia a branch que o pull request tem como alvo.

Execuções agendadas#

Uma execução agendada é uma execução própria, no head da branch padrão, e os seus jobs trazem a ref da branch padrão, como refs/heads/main.

Pull requests de um fork#

Um job em um pull request de um fork não recebe token de identidade. Só código enviado por push ao seu repositório pode pedir um.

O que uma política de confiança deve comparar#

  • Deploys: compare o subject exato que faz o deploy. Uma role de deploy para um job que nomeia um ambiente compara org:<workspace id>:repo:<owner>/<name>:environment:production; uma para uma branch compara org:<workspace id>:repo:<owner>/<name>:ref:refs/heads/main, ou uma tag de release. Um token nomeia refs/heads/main só em uma execução de um commit que está em main, então uma role ligada a main só roda código que está em main. Uma role de deploy nunca lista a forma advisory:.
  • Nunca compare uma ref de pull request para nada que importe. refs/pull/<number>/head muda a cada número de pull request e não aparece em todo job de pull request.
  • Uma role que qualquer job do repositório pode assumir (testes que leem um bucket de staging, builds de preview): compare todas as refs do repositório pelo prefixo org:<workspace id>:repo:<owner>/<name>:ref:. Esse prefixo não corresponde nem à forma de ambiente nem à forma advisory:, então uma role que também deve aceitar jobs que nomeiam um ambiente, ou execuções de ensaio e pipelines em ensaio, lista esses prefixos também.
  • Sempre compare o sub, com o id do seu workspace nele. Os tokens de todos os workspaces do Latchkey vêm do mesmo emissor, então uma confiança que verifica só o público, ou só o nome do repositório, também aceita os jobs de outro workspace.

As claims#

ClaimO que contém
isshttps://oidc.latchkey.dev
audO único público que o passo pediu
subO subject, em uma das quatro formas acima
iat, nbf, expQuando o token foi emitido, quando passa a valer e quando expira, em segundos
jtiUm id próprio para cada token
organization_idO id do seu workspace
repositoryO repositório como owner/name, como o Latchkey o leu por último do seu host de código
repository_idO id do repositório no Latchkey
job_idO job
run_id, attemptA execução, e a sua tentativa contando a partir de 1
refA ref da execução
shaO commit ao qual a execução está fixada
pipeline_id, step_keyO pipeline e o passo, que mantêm os seus ids em toda ref e todo commit
base_refA branch base de um pull request ou de um grupo de merge, como refs/heads/<branch>; em um push, o base_ref do próprio push quando o host de código envia um
actorQuem iniciou a execução
event_classO evento para o qual a execução foi planejada, como branch_push ou pr_update

Uma claim sem valor é deixada de fora em vez de ser enviada vazia, então uma regra que compara um valor vazio nunca corresponde por acidente.

Pedir um token em um passo#

Em um job que pode receber um token, todo passo tem duas variáveis: LATCHKEY_ID_TOKEN_REQUEST_URL, um endereço no próprio runner, e LATCHKEY_ID_TOKEN_REQUEST_TOKEN, a credencial para pedir a ele. Um job de um pull request de um fork não tem nenhuma das duas; qualquer outro job que não pode receber um token é recusado com uma das palavras abaixo. Acrescente &audience= e o público ao endereço, envie a credencial como bearer token, e o runner responde {"value": "<token>", "job_id": "<job id>"}:

yaml
- name: Get an identity token for AWS
  run: |
    answer=$(curl -sS --noproxy '*' -H "Authorization: Bearer $LATCHKEY_ID_TOKEN_REQUEST_TOKEN" \
      "$LATCHKEY_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
    if ! token=$(jq -er .value <<<"$answer" 2>/dev/null); then
      echo "No identity token: ${answer:-no answer}" >&2
      exit 1
    fi

Com o CLI 0.9.0 ou posterior instalado no job (npm install -g @latchkeydev/cli), token=$(latchkey oidc request-token --audience sts.amazonaws.com) faz a mesma requisição. Ele codifica o público por conta própria, mantém a requisição no runner e sai com exit 1 e o motivo quando não há token; --out <file> grava o token em um arquivo que só o dono pode ler, em vez de imprimi-lo.

O público é uma única string de 1 a 255 caracteres ASCII visíveis, sem espaço; codifique em porcentagem um público que contenha &, +, # ou %. O runner esconde o token no log do job, e no que o autorreparo lê, antes de o passo recebê-lo. Peça o mesmo público de novo e o runner responde o token que guarda enquanto resta pelo menos um terço da sua validade, e um novo depois disso. --noproxy '*' mantém a requisição no runner quando o seu job define um proxy.

Uma requisição sem a credencial recebe um 401 sem corpo, e uma que envia qualquer coisa além do público, um 400 sem corpo. Quando o Latchkey não emite um token, o runner responde um status de erro com {"refusal": "<word>"}, depois de tentar de novo por até 25 segundos quando uma nova tentativa pode ajudar:

PalavraO que significa
job_class_ineligibleO job não roda um commit do próprio repositório em uma execução do seu arquivo de workflow, como um job iniciado com latchkey run ou uma execução de uma correção que o autorreparo propôs
job_not_runningO job não está rodando
configuration_incompleteO Latchkey ainda não está configurado para emitir tokens aqui
claims_unavailable, signing_failed, ledger_unavailable, unexpected, mint_unavailableO Latchkey não conseguiu emitir o token agora; rode o passo de novo
mint_refusedO Latchkey recusou a requisição: o público não é um que ele aceita, ou a credencial do próprio job foi recusada, como quando o job já terminou
masker_refusedNão foi possível esconder o token no log, então ele não foi entregue ao passo

AWS#

Crie um provedor OpenID Connect para o emissor uma vez em cada conta AWS, com o público sts.amazonaws.com. Deixe de fora a thumbprint: a AWS verifica o certificado do emissor com as autoridades certificadoras em que confia.

terminal
1aws iam create-open-id-connect-provider \
2 --url https://oidc.latchkey.dev \
3 --client-id-list sts.amazonaws.com

Depois, confie no provedor a partir da role. Das claims que um token do Latchkey traz, a AWS só consegue comparar sub e aud, então tudo de que uma role depende precisa estar no subject que ela compara. Esta role de deploy aceita só jobs de acme/api que nomeiam o ambiente production, em execuções que um owner ou admin aprovou ou, quando production não precisa de aprovação, em execuções nas branches dele:

deploy-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.latchkey.dev"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.latchkey.dev:aud": "sts.amazonaws.com",
          "oidc.latchkey.dev:sub": "org:<workspace id>:repo:acme/api:environment:production"
        }
      }
    }
  ]
}

Uma role somente leitura que um teste usa para ler pode aceitar qualquer job do repositório que não nomeia nenhum ambiente, incluindo as execuções de ensaio e os pipelines em ensaio. StringLike lê * como qualquer sequência de caracteres, e a sua lista corresponde quando qualquer um dos seus valores corresponde:

read-only-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.latchkey.dev"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.latchkey.dev:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "oidc.latchkey.dev:sub": [
            "org:<workspace id>:repo:acme/api:ref:*",
            "advisory:org:<workspace id>:repo:acme/api:ref:*"
          ]
        }
      }
    }
  ]
}

No job, faça login com latchkey/aws-login. Ele pede o token, faz login na role e mantém as credenciais renovadas para todos os passos seguintes:

.latchkey/workflows/deploy.yml
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: small
    environment: production
    steps:
      - uses: latchkey/aws-login@1.0.0
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy
          aws-region: us-east-1
      - run: aws sts get-caller-identity