Commit 2adda703 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch '214607-ci-jwt-signing-key/jwks' into 'master'

Use dedicated signing key for CI_JOB_JWT

See merge request gitlab-org/gitlab!34249
parents 03c02cb9 7947a3c6
# 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 ...@@ -39,7 +39,8 @@ def create_tokens
secret_key_base: file_secret_key || generate_new_secure_token, secret_key_base: file_secret_key || generate_new_secure_token,
otp_key_base: env_secret_key || 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, 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) missing_secrets = set_missing_keys(defaults)
......
...@@ -171,9 +171,8 @@ Rails.application.routes.draw do ...@@ -171,9 +171,8 @@ Rails.application.routes.draw do
resources :abuse_reports, only: [:new, :create] resources :abuse_reports, only: [:new, :create]
# JWKS (JSON Web Key Set) endpoint # JWKS (JSON Web Key Set) endpoint
# Used by third parties to verify CI_JOB_JWT, placeholder route # Used by third parties to verify CI_JOB_JWT
# in case we decide to move away from doorkeeper-openid_connect get 'jwks' => 'jwks#index'
get 'jwks' => 'doorkeeper/openid_connect/discovery#keys'
end end
# End of the /-/ scope. # End of the /-/ scope.
......
...@@ -50,7 +50,7 @@ The JWT's payload looks like this: ...@@ -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. 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 ...@@ -60,7 +60,7 @@ module Gitlab
end end
def key 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 end
def public_key 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 ...@@ -37,10 +37,10 @@ describe 'create_tokens' do
expect(keys).to all(match(hex_key)) expect(keys).to all(match(hex_key))
end 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 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.uniq).to eq(keys)
expect(keys).to all(match(rsa_key)) expect(keys).to all(match(rsa_key))
...@@ -51,6 +51,7 @@ describe 'create_tokens' do ...@@ -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('otp_key_base')
expect(self).to receive(:warn_missing_secret).with('db_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('openid_connect_signing_key')
expect(self).to receive(:warn_missing_secret).with('ci_jwt_signing_key')
create_tokens create_tokens
end end
...@@ -63,6 +64,7 @@ describe 'create_tokens' do ...@@ -63,6 +64,7 @@ describe 'create_tokens' do
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) 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['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['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 end
create_tokens create_tokens
...@@ -79,6 +81,7 @@ describe 'create_tokens' do ...@@ -79,6 +81,7 @@ describe 'create_tokens' do
before do before do
secrets.db_key_base = 'db_key_base' secrets.db_key_base = 'db_key_base'
secrets.openid_connect_signing_key = 'openid_connect_signing_key' 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(:exist?).with('.secret').and_return(true)
allow(File).to receive(:read).with('.secret').and_return('file_key') allow(File).to receive(:read).with('.secret').and_return('file_key')
...@@ -90,6 +93,7 @@ describe 'create_tokens' do ...@@ -90,6 +93,7 @@ describe 'create_tokens' do
secrets.secret_key_base = 'secret_key_base' secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base' secrets.otp_key_base = 'otp_key_base'
secrets.openid_connect_signing_key = 'openid_connect_signing_key' secrets.openid_connect_signing_key = 'openid_connect_signing_key'
secrets.ci_jwt_signing_key = 'ci_jwt_signing_key'
end end
it 'does not issue a warning' do it 'does not issue a warning' do
...@@ -116,6 +120,7 @@ describe 'create_tokens' do ...@@ -116,6 +120,7 @@ describe 'create_tokens' do
secrets.secret_key_base = 'secret_key_base' secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base' secrets.otp_key_base = 'otp_key_base'
secrets.openid_connect_signing_key = 'openid_connect_signing_key' secrets.openid_connect_signing_key = 'openid_connect_signing_key'
secrets.ci_jwt_signing_key = 'ci_jwt_signing_key'
end end
it 'does not write any files' do it 'does not write any files' do
...@@ -131,6 +136,7 @@ describe 'create_tokens' do ...@@ -131,6 +136,7 @@ describe 'create_tokens' do
expect(secrets.otp_key_base).to eq('otp_key_base') expect(secrets.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_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.openid_connect_signing_key).to eq('openid_connect_signing_key')
expect(secrets.ci_jwt_signing_key).to eq('ci_jwt_signing_key')
end end
it 'deletes the .secret file' do it 'deletes the .secret file' do
...@@ -155,6 +161,7 @@ describe 'create_tokens' do ...@@ -155,6 +161,7 @@ describe 'create_tokens' do
expect(new_secrets['otp_key_base']).to eq('file_key') expect(new_secrets['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base') 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['openid_connect_signing_key']).to eq('openid_connect_signing_key')
expect(new_secrets['ci_jwt_signing_key']).to eq('ci_jwt_signing_key')
end end
create_tokens create_tokens
......
...@@ -93,7 +93,7 @@ describe Gitlab::Ci::Jwt do ...@@ -93,7 +93,7 @@ describe Gitlab::Ci::Jwt do
end end
describe '.for_build' do 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) } subject(:jwt) { described_class.for_build(build) }
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
require 'spec_helper' require 'spec_helper'
# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys # 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_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 # oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
...@@ -18,10 +17,6 @@ describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do ...@@ -18,10 +17,6 @@ describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
it "to #keys" do it "to #keys" do
expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys') expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
end end
it "/-/jwks" do
expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
end end
# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show # oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
......
...@@ -368,3 +368,10 @@ describe AutocompleteController, 'routing' do ...@@ -368,3 +368,10 @@ describe AutocompleteController, 'routing' do
expect(get("/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis') expect(get("/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis')
end end
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