Commit 7947a3c6 authored by Krasimir Angelov's avatar Krasimir Angelov

Use dedicated signing key for CI_JOB_JWT

Replace openid_connect_signing_key with the new ci_jwt_signing_key when
generating CI_JOB_JWT.

This also implements /-/jwks endpoint instead of delegating to
Doorkeper. Response will still include openid_connect_signing_key for
seamless rollout.

Related to https://gitlab.com/gitlab-org/gitlab/-/issues/214607.
parent 1d375778
# frozen_string_literal: true
class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController
def index
render json: { keys: keys }
end
private
def keys
[
# We keep openid_connect_signing_key so that we can seamlessly
# replace it with ci_jwt_signing_key and remove it on the next release.
# TODO: Remove openid_connect_signing_key in 13.2
# https://gitlab.com/gitlab-org/gitlab/-/issues/221031
Rails.application.secrets.openid_connect_signing_key,
Rails.application.secrets.ci_jwt_signing_key
].compact.map do |key_data|
OpenSSL::PKey::RSA.new(key_data)
.public_key
.to_jwk
.slice(:kty, :kid, :e, :n)
.merge(use: 'sig', alg: 'RS256')
end
end
end
---
title: Use dedicated RSA key to sign CI_JOB_JWT
merge_request: 34249
author:
type: added
......@@ -39,7 +39,8 @@ def create_tokens
secret_key_base: file_secret_key || generate_new_secure_token,
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
db_key_base: generate_new_secure_token,
openid_connect_signing_key: generate_new_rsa_private_key
openid_connect_signing_key: generate_new_rsa_private_key,
ci_jwt_signing_key: generate_new_rsa_private_key
}
missing_secrets = set_missing_keys(defaults)
......
......@@ -171,9 +171,8 @@ Rails.application.routes.draw do
resources :abuse_reports, only: [:new, :create]
# JWKS (JSON Web Key Set) endpoint
# Used by third parties to verify CI_JOB_JWT, placeholder route
# in case we decide to move away from doorkeeper-openid_connect
get 'jwks' => 'doorkeeper/openid_connect/discovery#keys'
# Used by third parties to verify CI_JOB_JWT
get 'jwks' => 'jwks#index'
end
# End of the /-/ scope.
......
......@@ -50,7 +50,7 @@ The JWT's payload looks like this:
}
```
The JWT is encoded by using RS256 and signed with your GitLab instance's OpenID Connect private key. The expire time for the token will be set to job's timeout, if specifed, or 5 minutes if it is not. The key used to sign this token may change without any notice. In such case retrying the job will generate new JWT using the current signing key.
The JWT is encoded by using RS256 and signed with a dedicated RSA private key. The expire time for the token will be set to job's timeout, if specifed, or 5 minutes if it is not. The key used to sign this token may change without any notice. In such case retrying the job will generate new JWT using the current signing key.
You can use this JWT and your instance's JWKS endpoint (`https://gitlab.example.com/-/jwks`) to authenticate with a Vault server that is configured to allow the JWT Authentication method for authentication.
......
......@@ -60,7 +60,7 @@ module Gitlab
end
def key
@key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key)
@key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.ci_jwt_signing_key)
end
def public_key
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JwksController do
describe 'GET #index' do
let(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk }
let(:ci_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.ci_jwt_signing_key).to_jwk }
it 'returns signing keys used to sign CI_JOB_JWT' do
get :index
expect(response).to have_gitlab_http_status(:ok)
ids = json_response['keys'].map { |jwk| jwk['kid'] }
expect(ids).to contain_exactly(ci_jwk['kid'], oidc_jwk['kid'])
end
it 'does not leak private key data' do
get :index
aggregate_failures do
json_response['keys'].each do |jwk|
expect(jwk.keys).to contain_exactly('kty', 'kid', 'e', 'n', 'use', 'alg')
expect(jwk['use']).to eq('sig')
expect(jwk['alg']).to eq('RS256')
end
end
end
end
end
......@@ -37,10 +37,10 @@ describe 'create_tokens' do
expect(keys).to all(match(hex_key))
end
it 'generates an RSA key for openid_connect_signing_key' do
it 'generates an RSA key for openid_connect_signing_key and ci_jwt_signing_key' do
create_tokens
keys = secrets.values_at(:openid_connect_signing_key)
keys = secrets.values_at(:openid_connect_signing_key, :ci_jwt_signing_key)
expect(keys.uniq).to eq(keys)
expect(keys).to all(match(rsa_key))
......@@ -51,6 +51,7 @@ describe 'create_tokens' do
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
expect(self).to receive(:warn_missing_secret).with('db_key_base')
expect(self).to receive(:warn_missing_secret).with('openid_connect_signing_key')
expect(self).to receive(:warn_missing_secret).with('ci_jwt_signing_key')
create_tokens
end
......@@ -63,6 +64,7 @@ describe 'create_tokens' do
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
expect(new_secrets['openid_connect_signing_key']).to eq(secrets.openid_connect_signing_key)
expect(new_secrets['ci_jwt_signing_key']).to eq(secrets.ci_jwt_signing_key)
end
create_tokens
......@@ -79,6 +81,7 @@ describe 'create_tokens' do
before do
secrets.db_key_base = 'db_key_base'
secrets.openid_connect_signing_key = 'openid_connect_signing_key'
secrets.ci_jwt_signing_key = 'ci_jwt_signing_key'
allow(File).to receive(:exist?).with('.secret').and_return(true)
allow(File).to receive(:read).with('.secret').and_return('file_key')
......@@ -90,6 +93,7 @@ describe 'create_tokens' do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
secrets.openid_connect_signing_key = 'openid_connect_signing_key'
secrets.ci_jwt_signing_key = 'ci_jwt_signing_key'
end
it 'does not issue a warning' do
......@@ -116,6 +120,7 @@ describe 'create_tokens' do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
secrets.openid_connect_signing_key = 'openid_connect_signing_key'
secrets.ci_jwt_signing_key = 'ci_jwt_signing_key'
end
it 'does not write any files' do
......@@ -131,6 +136,7 @@ describe 'create_tokens' do
expect(secrets.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_key_base')
expect(secrets.openid_connect_signing_key).to eq('openid_connect_signing_key')
expect(secrets.ci_jwt_signing_key).to eq('ci_jwt_signing_key')
end
it 'deletes the .secret file' do
......@@ -155,6 +161,7 @@ describe 'create_tokens' do
expect(new_secrets['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base')
expect(new_secrets['openid_connect_signing_key']).to eq('openid_connect_signing_key')
expect(new_secrets['ci_jwt_signing_key']).to eq('ci_jwt_signing_key')
end
create_tokens
......
......@@ -93,7 +93,7 @@ describe Gitlab::Ci::Jwt do
end
describe '.for_build' do
let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) }
let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.ci_jwt_signing_key) }
subject(:jwt) { described_class.for_build(build) }
......
......@@ -3,7 +3,6 @@
require 'spec_helper'
# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
# jwks GET /-/jwks(.:format) doorkeeper/openid_connect/discovery#keys
# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
......@@ -18,10 +17,6 @@ describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
it "to #keys" do
expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
it "/-/jwks" do
expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
end
# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
......
......@@ -368,3 +368,10 @@ describe AutocompleteController, 'routing' do
expect(get("/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis')
end
end
# jwks GET /-/jwks(.:format) jwks#index
describe JwksController, "routing" do
it "to #index" do
expect(get('/-/jwks')).to route_to('jwks#index')
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment