Skip to content

Commit ba3b860

Browse files
committed
Make Dependabot secret store opt-in per secret
Previously every secret was written to both the Actions and Dependabot secret stores. Write to Actions by default and only to Dependabot when a secret sets `dependabot: true`, so Actions-only credentials are not exposed to the Dependabot attack surface. Skip fetching the Dependabot public key when no secret opts in.
1 parent 59ea70e commit ba3b860

2 files changed

Lines changed: 105 additions & 12 deletions

File tree

lib/rake_github/tasks/secrets/provision.rb

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,25 @@ def provision_actions_secrets(client, task)
4242
end
4343

4444
def provision_dependabot_secrets(client, task)
45+
secrets = task.secrets.select { |secret| secret[:dependabot] }
46+
return if secrets.empty?
47+
4548
public_key = client.get_dependabot_public_key(task.repository)
46-
task.secrets.each do |secret|
47-
$stdout.print "Adding '#{secret[:name]}' to Dependabot... "
48-
client.create_or_update_dependabot_secret(
49-
task.repository,
50-
secret[:name],
51-
secret_options(public_key, secret[:value])
52-
)
53-
$stdout.puts 'Done.'
49+
secrets.each do |secret|
50+
write_dependabot_secret(client, task, public_key, secret)
5451
end
5552
end
5653

54+
def write_dependabot_secret(client, task, public_key, secret)
55+
$stdout.print "Adding '#{secret[:name]}' to Dependabot... "
56+
client.create_or_update_dependabot_secret(
57+
task.repository,
58+
secret[:name],
59+
secret_options(public_key, secret[:value])
60+
)
61+
$stdout.puts 'Done.'
62+
end
63+
5764
def secret_options(public_key, value)
5865
{
5966
key_id: public_key.key_id,

spec/rake_github/tasks/secrets/provision_spec.rb

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,69 @@ def define_task(opts = {}, &block)
116116
end
117117
# rubocop:enable RSpec/MultipleExpectations
118118

119+
it 'does not write to the dependabot store by default' do
120+
repository = 'org/repo'
121+
122+
client = stub_github_client
123+
124+
define_task(
125+
repository:,
126+
access_token: 'some-token',
127+
secrets: [{ name: 'SOME_SECRET', value: 'some-value' }]
128+
)
129+
130+
Rake::Task['secrets:provision'].invoke
131+
132+
expect(client)
133+
.not_to(have_received(:create_or_update_dependabot_secret))
134+
end
135+
136+
it 'does not fetch the dependabot public key when no secret opts in' do
137+
client = stub_github_client
138+
139+
define_task(
140+
repository: 'org/repo',
141+
access_token: 'some-token',
142+
secrets: [{ name: 'SOME_SECRET', value: 'some-value' }]
143+
)
144+
145+
Rake::Task['secrets:provision'].invoke
146+
147+
expect(client).not_to(have_received(:get_dependabot_public_key))
148+
end
149+
150+
# rubocop:disable RSpec/MultipleExpectations
151+
it 'writes every secret to actions but only opted-in secrets to dependabot' do
152+
repository = 'org/repo'
153+
154+
client = stub_github_client
155+
156+
define_task(
157+
repository:,
158+
access_token: 'some-token',
159+
secrets: [
160+
{ name: 'ACTIONS_ONLY', value: 'value-one' },
161+
{ name: 'BOTH', value: 'value-two', dependabot: true }
162+
]
163+
)
164+
165+
Rake::Task['secrets:provision'].invoke
166+
167+
expect(client)
168+
.to(have_received(:create_or_update_actions_secret)
169+
.with(repository, 'ACTIONS_ONLY', anything))
170+
expect(client)
171+
.to(have_received(:create_or_update_actions_secret)
172+
.with(repository, 'BOTH', anything))
173+
expect(client)
174+
.to(have_received(:create_or_update_dependabot_secret)
175+
.with(repository, 'BOTH', anything))
176+
expect(client)
177+
.not_to(have_received(:create_or_update_dependabot_secret)
178+
.with(repository, 'ACTIONS_ONLY', anything))
179+
end
180+
# rubocop:enable RSpec/MultipleExpectations
181+
119182
it 'creates or updates the dependabot secret with a sealed value' do
120183
repository = 'org/repo'
121184
access_token = 'some-token'
@@ -129,7 +192,7 @@ def define_task(opts = {}, &block)
129192
define_task(
130193
repository:,
131194
access_token:,
132-
secrets: [{ name: 'SOME_SECRET', value: 'some-value' }]
195+
secrets: [{ name: 'SOME_SECRET', value: 'some-value', dependabot: true }]
133196
)
134197

135198
Rake::Task['secrets:provision'].invoke
@@ -155,7 +218,7 @@ def define_task(opts = {}, &block)
155218
define_task(
156219
repository:,
157220
access_token: 'some-token',
158-
secrets: [{ name: 'SOME_SECRET', value: 'some-value' }]
221+
secrets: [{ name: 'SOME_SECRET', value: 'some-value', dependabot: true }]
159222
)
160223

161224
Rake::Task['secrets:provision'].invoke
@@ -192,6 +255,29 @@ def define_task(opts = {}, &block)
192255
hash_including(key_id: 'actions-key-id')))
193256
end
194257

258+
it 'passes the fetched key id to the dependabot secret' do
259+
repository = 'org/repo'
260+
private_key = RbNaCl::PrivateKey.generate
261+
262+
client = stub_github_client
263+
stub_dependabot_public_key(
264+
client, repository, 'dependabot-key-id', private_key
265+
)
266+
267+
define_task(
268+
repository:,
269+
access_token: 'some-token',
270+
secrets: [{ name: 'SOME_SECRET', value: 'some-value', dependabot: true }]
271+
)
272+
273+
Rake::Task['secrets:provision'].invoke
274+
275+
expect(client)
276+
.to(have_received(:create_or_update_dependabot_secret)
277+
.with(repository, 'SOME_SECRET',
278+
hash_including(key_id: 'dependabot-key-id')))
279+
end
280+
195281
# rubocop:disable RSpec/MultipleExpectations
196282
it 'fetches each public key once per run rather than per secret' do
197283
repository = 'org/repo'
@@ -207,8 +293,8 @@ def define_task(opts = {}, &block)
207293
repository:,
208294
access_token: 'some-token',
209295
secrets: [
210-
{ name: 'SECRET_ONE', value: 'value-one' },
211-
{ name: 'SECRET_TWO', value: 'value-two' }
296+
{ name: 'SECRET_ONE', value: 'value-one', dependabot: true },
297+
{ name: 'SECRET_TWO', value: 'value-two', dependabot: true }
212298
]
213299
)
214300

0 commit comments

Comments
 (0)