Skip to content
Snippets Groups Projects

Implement secrets manager client JWT auth

Merged Erick Bajao requested to merge eb-rails-openbao-auth into master

Resolves #510131 (closed)

Background

Currently, GitLab's secrets management system communicates with OpenBao through a proxy server. While this works, it adds complexity to our infrastructure and creates an additional point of failure. We want to simplify this by having GitLab authenticate directly with OpenBao using JSON Web Tokens (JWT).

What's Changing

This change introduces JWT-based authentication for GitLab's communication with OpenBao, eliminating the need for the proxy server. A key advantage of JWT-based authentication over AppRole is that JWTs can carry rich contextual information that enhances our audit capabilities.

GitLab already has JWT generation classes (Jwt and JwtV2), but these are specifically designed for CI/CD build authentication. They include CI-specific claims and behaviors that aren't relevant for secrets management. Instead of adapting these classes, we've created a new SecretsManagerJwt class that extends from JwtBase. This gives us a clean implementation focused solely on generating tokens for OpenBao authentication, while still leveraging GitLab's core JWT infrastructure.

Our JWT implementation includes rich contextual information in its claims that helps us understand and audit every interaction with OpenBao:

  • User identity for tracking who performed each operation
  • Project context for understanding the scope of operations
  • Standard JWT claims for security validation and token expiration

This level of detail in our authentication tokens means that every operation in OpenBao can be traced back to the specific user and project that initiated it, providing much better audit trails than would be possible with AppRole authentication. When investigating security events or understanding system usage, this granular information becomes invaluable.

We've also reorganized the code to properly handle JWT authentication:

  • Moved OpenBao client interactions to services and finders where user context is available
  • Updated SecretsManagerClient to authenticate using these custom JWTs

Why Services

Moving client interactions to services isn't just about code organization - it's driven by the need for proper JWT authentication. These layers naturally have access to user context (i.e. current_user) needed for generating JWTs, making them the right place to handle OpenBao communication. Injecting this user context into the model layer would be architecturally awkward, as models should focus on representing domain objects and their validation rules rather than knowing about the current user making a request.

For example, when we create a secret, the service already knows about current_user through GitLab's service pattern. This makes it a natural place to generate JWTs and handle authenticated communication with OpenBao. The model, on the other hand, can stay focused on what makes a secret valid - its name, description, environment rules, etc. - without needing to understand anything about the user performing the operation.

How to test locally

I used the openbao server from GDK and didn't setup the proxy anymore.

Then I set up the needed JWT and policy configurations through the bao CLI:

First, I enabled the JWT authentication method in OpenBao:

bao auth enable -path=gitlab_rails_jwt jwt

Then I configured the JWT authentication with my GDK GitLab OIDC discovery URL and the expected issuer:

bao write auth/gitlab_rails_jwt/config \
    oidc_discovery_url="http://gdk.test:3000" \
    bound_issuer="http://gdk.test:3000"

I created a policy that grants comprehensive permissions for managing secrets:

bao policy write secrets_manager - <<EOF
path "*" {
  capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
EOF

Finally, I created a JWT role app that validates tokens and assigns the appropriate policy. This role specifies which JWT claims to expect and what permissions to grant upon successful authentication:

bao write auth/gitlab_rails_jwt/role/app \
    role_type=jwt \
    bound_audiences=openbao \
    user_claim=user_id \
    token_policies=secrets_manager

After this, I was able to authenticate with JWT within Rails without the help of the proxy.

Some sample queries and mutations I tested this on:

mutation {
  projectSecretsManagerInitialize(input: {projectPath:"root/test-openbao"}) {
    projectSecretsManager {
      status
      ciSecretsMountPath
    }
    errors
  }
}

mutation {
  projectSecretCreate(input: {
    projectPath:"root/test-openbao",
    name: "testnoproxy_secret",
    description:"a new secret to test",
    environment:"staging",
    branch:"development",
    value:"testnoproxyvalue"
  }) {
    projectSecret {
      name
    }
    errors
  }
}

query {
  projectSecrets(projectPath: "root/test-openbao"){
    nodes {
      name
      description
      environment
      branch
    }
  }
}

mutation {
  projectSecretDelete(input: {
    projectPath:"root/test-openbao",
    name: "testnoproxy_secret"
  }) {
    projectSecret {
      name
    }
    errors
  }
}
Edited by Erick Bajao

Merge request reports

Merge train pipeline #1710845355 passed

Merge train pipeline passed for a7e048b2

Approval is optional
Ready to merge by members who can write to the target branch.

Activity

Filter activity
  • Approvals
  • Assignees & reviewers
  • Comments (from bots)
  • Comments (from users)
  • Commits & branches
  • Edits
  • Labels
  • Lock status
  • Mentions
  • Merge request status
  • Tracking
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
Please register or sign in to reply
Loading