Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: site: Add GitLab CI/CD example for JWT Auth #2146

Open
Dentrax opened this issue Nov 28, 2024 · 12 comments
Open

docs: site: Add GitLab CI/CD example for JWT Auth #2146

Dentrax opened this issue Nov 28, 2024 · 12 comments

Comments

@Dentrax
Copy link

Dentrax commented Nov 28, 2024

Fresh user here!

I was reading the Using Pinniped for CI/CD cluster operations doc. It'd be great to expand this doc by providing some demo instructions about how can we use JWT Auth with GitLab to access kubeconfig from the pipeline.

As a reference, I can drop GitLab's Use HashiCorp Vault secrets in GitLab CI/CD and OpenID Connect (OIDC) Authentication Using ID Tokens docs.

Thanks.

@cfryanr
Copy link
Member

cfryanr commented Dec 2, 2024

Hi @Dentrax! Thanks for posting this suggestion.

Pinniped abstracts its internal usage of various token types (e.g. JWTs) so that hopefully you don't need to worry about the level of detail shown in either of those GitLab docs.

Here's an outline of the steps if you are using the Pinniped Supervisor:

Then it should "just work" for your users to be able to authenticate without requiring any interactive prompts by using that kubeconfig with the environment variables to hold their username and password (see https://pinniped.dev/docs/howto/cicd).

Is that what you were asking about? Or maybe you meant something else?

@Dentrax
Copy link
Author

Dentrax commented Dec 2, 2024

Thank you @cfryanr for your detailed explanations and links to useful resource!

Is that what you were asking about? Or maybe you meant something else?

After following the dos you shared, will I be able to obtain kubeconfigs for the cluster(s) in the GitLab CI/CD pipeline running with GitLab Runner? (non-TTY env)

@cfryanr
Copy link
Member

cfryanr commented Dec 2, 2024

Hi @Dentrax, sorry, I'm not too familiar with the details of GitLab Runner. What I outlined above would give you a kubeconfig that you could use from anywhere. You'd have to figure out how to provide that kubeconfig file to your CI/CD scripts (e.g. pull it from a git repo), but that kubeconfig would not have any credentials or identity included inside it, so it can be shared among all users of that cluster. By setting the env vars PINNIPED_USERNAME and PINNIPED_PASSWORD when using that kubeconfig, all interactive auth prompts will be skipped so no tty will be needed. So you would need to figure out how to provide the username and password of a user to your CI/CD scripts as those env vars, but hopefully that should be easy in any CI/CD system, since it is a common practice.

@Dentrax
Copy link
Author

Dentrax commented Dec 2, 2024

Ah thanks for PINNIPED_USERNAME and PINNIPED_PASSWORD hint! @cfryanr

Is this possible to make whole process KEYLESS? Are those variables mandatory? Why should we export those?

For example the Vault use-case, I don't export any USERNAME or PASSWORD thing in the CI/CD variables. Both Vault and GitLab verifies each other for trust, enabling keyless authentication with JWT. I was wondering if the same thing is possible with using the Pinniped Supervisor.

I can generate a kubeconfig on the CI, no problem. In my GitLab CI/CD pipeline case, I was mostly asking for, what variable should I particularly pass to pinniped login oidc <FLAGS> (Same as here? https://pinniped.dev/docs/howto/concierge/configure-concierge-jwt/, as you mentioned above?)

GitLab also exports ID_TOKENs, but I couldn't get which flag should I use to pass this variable. (I have read the doc above and there was no flag to pass id_token)

@cfryanr
Copy link
Member

cfryanr commented Dec 2, 2024

Is this possible to make whole process KEYLESS? Are those variables mandatory?

Pinniped's primary use case is to provide authentication for human users, where those identities are sourced from external identity providers (e.g. GitLab, AD, GitHub, etc.). So you would typically only use Pinniped for non-human auth (e.g. CI bot accounts) when you prefer that those identities are also sourced from an external identity provider in a fashion similar to your human users. That's why you need to provide a username and password to auth in the typical flow. Some companies prefer this approach because then they can have a single, consistent point of control for all human and non-human actors. However, using Pinniped for human auth does not disable any other forms of auth that are supported by your clusters, so you can still use Pinniped for human auth while using other solutions for service-to-service auth (non-human auth) at the same time.

GitLab also exports ID_TOKENs, but I couldn't get which flag should I use to pass this variable.

Maybe you're asking something else that I'm not understanding. Have you already acquired a JWT issued by GitLab somehow using some existing automation unrelated to Pinniped, and are you trying to use that JWT alone to authenticate to a cluster? Sorry, I'm not sure if I understand your use case. In the typical Pinniped use case, we do not assume that you start with a JWT. We assume that every user starts completely unauthenticated, with nothing but a Pinniped-generated kubeconfig (which contains no credentials and no identity), and then Pinniped helps you authenticate and get access to clusters. Could you describe more about what you are trying to accomplish, without assuming that the reader has any knowledge of your specific tools?

@Dentrax
Copy link
Author

Dentrax commented Dec 2, 2024

Thank you @cfryanr! Really appreciate you trying to understand my use case and providing further details.

Could you describe more about what you are trying to accomplish, without assuming that the reader has any knowledge of your specific tools?

Happy to describe more! I've tried to draw some flow diagram here: (What I'm trying to accomplish)

pinniped-gitlab

Have you already acquired a JWT issued by GitLab somehow using some existing automation unrelated to Pinniped

GitLab already exports this with id_tokens by default

and are you trying to use that JWT alone to authenticate to a cluster?

Exactly! If pinniped supervisor verifies this using GitLab's JWKS, we should able to auth to cluster

In the typical Pinniped use case, we do not assume that you start with a JWT. We assume that every user starts completely unauthenticated, with nothing but a Pinniped-generated kubeconfig (which contains no credentials and no identity), and then Pinniped helps you authenticate and get access to clusters.

Yeah actually, in this scenario, the flow starts with an ID token. Is this something that doesn't fit with the current design/architecture?

I'm not so familiar end-to-end OIDC/JWT flow and terminology, sorry if I'm mixing things up here. Thanks in advance.

@cfryanr
Copy link
Member

cfryanr commented Dec 3, 2024

Hi @Dentrax, thanks for the diagram. That helped me understand your use case. Here is what I think might work:

  1. On the target cluster, install the Pinniped Concierge and configure a JWTAuthenticator to validate these special ID tokens from GitLab. Be careful to set all the configuration options on the JWTAuthenticator correctly. Note that this will allow anyone who can get ahold of one of those JWTs to authenticate into the cluster for the lifetime of that token. Hopefully you can make those tokens short-lived and restrict access to those tokens. Hopefully that token can have an audience value which is unique both for this use case and for that specific target cluster, to make the token worthless for other use cases and against other target clusters (not a requirement, but good for security).
  2. With the cluster admin's kubeconfig, use pinniped get kubeconfig to generate a new kubeconfig for that cluster, but use the --static-token-env flag to choose the name of an environment variable where the special ID token will reside inside the CI job, e.g. MY_ID_TOKEN. You can do this once for the cluster, so this does not need to happen as part of the CI job.
  3. Make sure that the pinniped CLI is available inside your CI job, along with the kubeconfig that you generated above.
  4. In the CI job, make sure that the value of the ID token is exported into an environment variable with the name that you chose above, e.g. MY_ID_TOKEN.
  5. In the CI job, make sure you are using the kubeconfig, e.g. by setting the KUBECONFIG environment variable or using the --kubeconfig flag, or however you prefer to accomplish that.
  6. Now the CI job should be able to use kubectl and other similar clients that respect the kubeconfig. It should authenticate automatically.

@joshuatcasey
Copy link
Member

joshuatcasey commented Dec 11, 2024

@cfryanr Just to clarify - it's possible to use a static token against a JWTAuthenticator? I've only seen it used against a WebhookAuthenticator.

I'm guessing this kubeconfig would need to run command pinniped login oidc which does't seem to have any input parameters for a static token. I'm not sure we have a way to "shortcut" the actual user login aspect of pinniped login oidc with a pre-existing token. I guess they could curl the TokenCredentialRequest with the token? Or build up their own sessions.yaml with a valid ID token and no refresh token, which would tell pinniped login oidc to shortcut the user login?

$ pinniped login oidc -h
Login using an OpenID Connect provider

Usage:
  pinniped login oidc --issuer ISSUER [flags]

Flags:
      --ca-bundle strings                        Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
      --ca-bundle-data strings                   Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
      --client-id string                         OpenID Connect client ID (default "pinniped-cli")
      --concierge-api-group-suffix string        Concierge API group suffix (default "pinniped.dev")
      --concierge-authenticator-name string      Concierge authenticator name
      --concierge-authenticator-type string      Concierge authenticator type (e.g., 'webhook', 'jwt')
      --concierge-ca-bundle-data string          CA bundle to use when connecting to the Concierge
      --concierge-endpoint string                API base for the Concierge endpoint
      --credential-cache string                  Path to cluster-specific credentials cache ("" disables the cache) (default "/Users/caseyj/.config/pinniped/credentials.yaml")
      --enable-concierge                         Use the Concierge to login
  -h, --help                                     help for oidc
      --issuer string                            OpenID Connect issuer URL
      --listen-port uint16                       TCP port for localhost listener (authorization code flow only)
      --request-audience string                  Request a token with an alternate audience using RFC8693 token exchange
      --scopes strings                           OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience,username,groups])
      --session-cache string                     Path to session cache file (default "/Users/caseyj/.config/pinniped/sessions.yaml")
      --skip-browser                             Skip opening the browser (just print the URL)
      --upstream-identity-provider-flow string   The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password')
      --upstream-identity-provider-name string   The name of the upstream identity provider used during login with a Supervisor
      --upstream-identity-provider-type string   The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') (default "oidc")

$ pinniped login static -h
Login using a static token

Usage:
  pinniped login static [--token TOKEN] [--token-env TOKEN_NAME] [flags]

Flags:
      --concierge-api-group-suffix string     Concierge API group suffix (default "pinniped.dev")
      --concierge-authenticator-name string   Concierge authenticator name
      --concierge-authenticator-type string   Concierge authenticator type (e.g., 'webhook', 'jwt')
      --concierge-ca-bundle-data string       CA bundle to use when connecting to the Concierge
      --concierge-endpoint string             API base for the Concierge endpoint
      --credential-cache string               Path to cluster-specific credentials cache ("" disables the cache) (default "/Users/caseyj/.config/pinniped/credentials.yaml")
      --enable-concierge                      Use the Concierge to login
  -h, --help                                  help for static
      --token string                          Static token to present during login
      --token-env string                      Environment variable containing a static token

@cfryanr
Copy link
Member

cfryanr commented Dec 12, 2024

When you run pinniped get kubeconfig --static-token-env MY_ID_TOKEN then it creates a kubeconfig that contains pinniped login static. That command will call the TokenCredentialRequest for you. It will embed the token from your env var into the body of the request.

This use case doesn't need pinniped login oidc because pinniped login oidc will help you do the authcode flow to get tokens, but in this case @Dentrax is starting with a token already in hand and doesn't need help acquiring one.

@developer-guy
Copy link

developer-guy commented Dec 19, 2024

I tried something like this. First I created a JWTAuthenticator like this:

apiVersion: authentication.concierge.pinniped.dev/v1alpha1
kind: JWTAuthenticator
metadata:
  name: demo-supervisor-jwt-authenticator-gitlab
spec:  # This should be the issuer URL that was declared in the FederationDomain.
  issuer: "https://gitlab.private.com"
  audience: workload1-dd9de13c370982f61e9f # the id of the workload cluster
  claims:
    username: email

Then I created a pipeline in GitLab like this:

manual_authentication:
  image: alpine:latest
  id_tokens:
    DEMO_ID_TOKEN:
      aud: workload1-dd9de13c370982f61e9f # the same audiance
  script:
  - apk add curl jq
  - |
    mkdir -p $HOME/.kube
    cat "$KUBECONFIG" > $HOME/.kube/config
    export KUBECONFIG=$HOME/.kube/config
    export PINNIPED_USERNAME=gitlab
    export PINNIPED_PASSWORD=$CI_JOB_TOKEN
    curl -Lso pinniped https://get.pinniped.dev/v0.36.0/pinniped-cli-linux-amd64 \
    && chmod +x pinniped \
    && mv pinniped /usr/local/bin/pinniped
    echo "${DEMO_ID_TOKEN}" |base64 > /tmp/web_identity_token
    cat /tmp/web_identity_token
    DEMO_TOKEN=$(cat /tmp/web_identity_token |base64 -d)
    pinniped get kubeconfig \
      --static-token $(cat /tmp/web_identity_token |base64 -d) \
      --concierge-api-group-suffix=pinniped.dev \
      --concierge-endpoint=https://x.x.x.x:6443 \
      --concierge-authenticator-name=demo-supervisor-jwt-authenticator-gitlab \
      --concierge-authenticator-type=jwt \
      --oidc-scopes openid,offline_access,email > /tmp/kubeconfig
    cat /tmp/kubeconfig
    KUBECONFIG=/tmp/kubeconfig pinniped whoami

But the pipeline fails with the following error:

Error: could not complete Concierge credential exchange: login failed: authentication failed
Error: could not complete WhoAmIRequest: Post "https://x.x.x.x:6443/apis/identity.concierge.pinniped.dev/v1alpha1/whoamirequests": getting credentials: exec: executable /usr/local/bin/pinniped failed with exit code 1

The ID_TOKEN includes these claims : https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload

I'm 100% sure I'm missing something due to lack of knowledge, would you mind helping us @cfryanr? Thank you!

/cc @Dentrax

btw

Here is the generated kubeconfig maybe it could help:

users:
- name: kubernetes-admin-pinniped
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - login
      - static
      - --enable-concierge
      - --concierge-api-group-suffix=pinniped.dev
      - --concierge-authenticator-name=demo-supervisor-jwt-authenticator-gitlab
      - --concierge-authenticator-type=jwt
      - --concierge-endpoint=https://x.x.x.x:6443
      - --concierge-ca-bundle-data=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFR...
      - --token=[MASKED]
      command: /usr/local/bin/pinniped
      env: []
      installHint: The pinniped CLI does not appear to be installed.  See https://get.pinniped.dev/cli
        for more details
      provideClusterInfo: true

@cfryanr
Copy link
Member

cfryanr commented Dec 19, 2024

@developer-guy I see a few problems with what you posted.

  1. According to the GitLab doc that you shared, there will be no email claim in the ID token. It will be called user_email instead. So if you want the value at that claim to become the user's Kubernetes username, then you would need to specify user_email as the JWTAuthenticator.spec.claims.username value.
  2. You don't need PINNIPED_USERNAME and PINNIPED_PASSWORD when you are using the ID token as the credential in this way. Those env vars are only for when you are using the Pinniped Supervisor for authentication.
  3. You shouldn't need the following arguments to pinniped get kubeconfig: --concierge-api-group-suffix=pinniped.dev --concierge-endpoint=https://x.x.x.x:6443 --oidc-scopes openid,offline_access,email

If it still doesn't work after making those changes, look at the pod logs for the Concierge pods at the same timestamp as the authentication failure. You should be able to get more information about why it failed.

@developer-guy
Copy link

it worked, thank you very much!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants