Skip to content

Commit 59dfb0f

Browse files
committed
Fix plugin secrets in config
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent 6d5a0bd commit 59dfb0f

10 files changed

Lines changed: 145 additions & 26 deletions

File tree

docs/developer/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ cd tests/checks
137137
./qrun.sh
138138
```
139139

140+
To run a specific integration test:
141+
142+
```bash
143+
cd tests/checks
144+
./qrun.sh <FOLDER>
145+
```
146+
140147
To test the documentation snippets:
141148

142149
```bash

docs/secrets.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ This feature allows you to decouple the use of secrets in your pipelines from th
1313

1414
When a pipeline is launched, Nextflow injects the secrets into the run without leaking them into temporary execution files. Secrets are provided to tasks as environment variables.
1515

16+
Secrets can be used with the local executor and grid executors (e.g., Slurm or Grid Engine). Secrets can be used with the AWS Batch executor when launched from [Seqera Platform](https://seqera.io/blog/pipeline-secrets-secure-handling-of-sensitive-information-in-tower/).
17+
1618
## Command line
1719

1820
The Nextflow {ref}`cli-secrets` sub-command can be used to manage secrets:
@@ -45,9 +47,32 @@ aws {
4547
The above snippet accesses the secrets `MY_ACCESS_KEY` and `MY_SECRET_KEY` and assigns them to the corresponding AWS config settings.
4648

4749
:::{warning}
48-
Secrets cannot be assigned to pipeline parameters.
50+
Secrets should not be assigned to pipeline parameters, as they can be leaked by the pipeline.
51+
:::
52+
53+
:::{versionadded} 25.10.0
4954
:::
5055

56+
Nextflow supports the use of secrets provided by plugins (e.g., AWS secrets) in configuration. However, due to the way that plugins are loaded, there are specific considerations when using config secrets:
57+
58+
- **Initial config load**: Nextflow first loads the configuration _without_ secrets enabled. Any reference to a secret will return the empty string `''`.
59+
60+
- **Plugin resolution**: Plugins are resolved after the initial configuration load. This is because the configuration can specify additional plugins.
61+
62+
- **Config reloading**: If secrets are accessed during configuration and the initial load succeeds, Nextflow will reload the configuration with secrets enabled.
63+
64+
As a result, config secrets must be used in a way that does not cause the config resolution to fail when secrets are not present.
65+
66+
For example:
67+
68+
```groovy
69+
includeConfig secrets.MY_SECRET
70+
? "https://example.com/extra.config?secret=${secrets.MY_SECRET}"
71+
: '/dev/null'
72+
```
73+
74+
The above snippet includes a secured config only if the secret is present. Otherwise, it includes `/dev/null`, which is equivalent to including an empty file. The reference to `secrets.MY_SECRET` in the condition causes the config to be reloaded with secrets enabled, including secrets from plugins such as AWS secrets.
75+
5176
(secrets-pipeline-script)=
5277

5378
## Pipeline script
@@ -67,10 +92,6 @@ workflow {
6792
The above example is only meant to demonstrate how to access a secret, not how to use it. In practice, sensitive information should not be printed to the console or output files.
6893
:::
6994

70-
:::{note}
71-
Secrets can only be used with the local or grid executors (e.g., Slurm or Grid Engine). Secrets can be used with the AWS Batch executor when launched from [Seqera Platform](https://seqera.io/blog/pipeline-secrets-secure-handling-of-sensitive-information-in-tower/).
72-
:::
73-
7495
## Process directive
7596

7697
Secrets can be accesses by processes using the {ref}`process-secret` directive. For example:
@@ -92,7 +113,3 @@ In the above example, the secrets `MY_ACCESS_KEY` and `MY_SECRET_KEY` are inject
92113
:::{warning}
93114
Secrets are made available as environment variables in the process script. To prevent evaluation in the Nextflow script context, escape variable names with a backslash (e.g., `\$MY_ACCESS_KEY`) as shown above.
94115
:::
95-
96-
:::{note}
97-
Secrets can only be used with the local or grid executors (e.g., Slurm or Grid Engine). Secrets can be used with the AWS Batch executor when launched from [Seqera Platform](https://seqera.io/blog/pipeline-secrets-secure-handling-of-sensitive-information-in-tower/).
98-
:::

modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import nextflow.plugin.Plugins
4444
import nextflow.scm.AssetManager
4545
import nextflow.script.ScriptFile
4646
import nextflow.script.ScriptRunner
47+
import nextflow.secret.ConfigNullProvider
48+
import nextflow.secret.SecretsLoader
4749
import nextflow.util.CustomPoolFactory
4850
import nextflow.util.Duration
4951
import nextflow.util.HistoryFile
@@ -320,31 +322,47 @@ class CmdRun extends CmdBase implements HubOptions {
320322
checkRunName()
321323

322324
printBanner()
323-
Plugins.init()
324325

325-
// -- specify the arguments
326+
// -- resolve main script
326327
final scriptFile = getScriptFile(pipeline)
327328

328329
// -- load command line params
329330
final baseDir = scriptFile.parent
330-
final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir))
331+
final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir, null))
332+
333+
// -- load config (without secrets)
334+
final secretsProvider = new ConfigNullProvider()
335+
ConfigBuilder builder = new ConfigBuilder()
336+
.setOptions(launcher.options)
337+
.setCmdRun(this)
338+
.setBaseDir(scriptFile.parent)
339+
.setCliParams(cliParams)
340+
.setSecretsProvider(secretsProvider)
341+
ConfigMap config = builder.build()
342+
Map configParams = builder.getConfigParams()
343+
344+
// -- load plugins
345+
Plugins.init()
346+
Plugins.load(config)
331347

332-
// create the config object
333-
final builder = new ConfigBuilder()
348+
// -- load secrets provider
349+
SecretsLoader.getInstance().load()
350+
351+
// -- reload config if secrets were used
352+
if( secretsProvider.usedSecrets() ) {
353+
log.debug "Config file used secrets -- reloading config with secrets provider"
354+
builder = new ConfigBuilder()
334355
.setOptions(launcher.options)
335356
.setCmdRun(this)
336-
.setBaseDir(baseDir)
357+
.setBaseDir(scriptFile.parent)
337358
.setCliParams(cliParams)
338-
final config = builder.build()
339-
final configParams = builder.getConfigParams()
359+
config = builder.build()
360+
configParams = builder.getConfigParams()
361+
}
340362

341363
// check DSL syntax in the config
342364
launchInfo(config, scriptFile)
343365

344-
// -- load plugins
345-
final cfg = plugins ? [plugins: plugins.tokenize(',')] : config
346-
Plugins.load(cfg)
347-
348366
// -- validate config options
349367
if( NF.isSyntaxParserV2() )
350368
new ConfigValidator().validate(config)

modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import nextflow.cli.CmdRun
3434
import nextflow.exception.AbortOperationException
3535
import nextflow.exception.ConfigParseException
3636
import nextflow.secret.SecretsLoader
37+
import nextflow.secret.SecretsProvider
3738
import nextflow.util.HistoryFile
3839
import nextflow.util.SecretHelper
3940
/**
@@ -76,6 +77,8 @@ class ConfigBuilder {
7677

7778
boolean showMissingVariables
7879

80+
SecretsProvider secretsProvider
81+
7982
Map<ConfigObject, String> emptyVariables = new LinkedHashMap<>(10)
8083

8184
Map<String,String> env = new HashMap<>(SysEnv.get())
@@ -104,6 +107,11 @@ class ConfigBuilder {
104107
return this
105108
}
106109

110+
ConfigBuilder setSecretsProvider(SecretsProvider value) {
111+
this.secretsProvider = value
112+
return this
113+
}
114+
107115
ConfigBuilder setOptions( CliOptions options ) {
108116
this.options = options
109117
return this
@@ -337,17 +345,20 @@ class ConfigBuilder {
337345
// this is needed to make sure to reuse the same
338346
// instance of the config vars across different instances of the ConfigBuilder
339347
// and prevent multiple parsing of the same params file (which can even be remote resource)
340-
return getConfigVars(baseDir)
348+
final secretContext = secretsProvider
349+
? SecretsLoader.secretContext(secretsProvider)
350+
: SecretsLoader.secretContext()
351+
return getConfigVars(baseDir, secretContext)
341352
}
342353

343354
@Memoized
344-
static Map getConfigVars(Path base) {
355+
static Map getConfigVars(Path base, Object secretContext) {
345356
final binding = new HashMap(10)
346357
binding.put('baseDir', base)
347358
binding.put('projectDir', base)
348359
binding.put('launchDir', Paths.get('.').toRealPath())
349360
binding.put('outputDir', Paths.get('results').complete())
350-
binding.put('secrets', SecretsLoader.secretContext())
361+
binding.put('secrets', secretContext)
351362
return binding
352363
}
353364

@@ -560,6 +571,9 @@ class ConfigBuilder {
560571
if( cmdRun.preview )
561572
config.preview = cmdRun.preview
562573

574+
if( cmdRun.plugins )
575+
config.plugins = cmdRun.plugins.tokenize(',')
576+
563577
// -- sets the working directory
564578
if( cmdRun.workDir )
565579
config.workDir = cmdRun.workDir
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package nextflow.secret
19+
20+
import groovy.transform.CompileStatic
21+
22+
/**
23+
* Specialization of the null secrets provider that is used to
24+
* determine whether secrets are required in the config.
25+
*
26+
* @author Ben Sherman <bentshermann@gmail.com>
27+
*/
28+
@CompileStatic
29+
class ConfigNullProvider extends NullProvider {
30+
31+
private boolean accessed
32+
33+
@Override
34+
Secret getSecret(String name) {
35+
accessed = true
36+
return new SecretImpl(name, '')
37+
}
38+
39+
boolean usedSecrets() {
40+
return accessed
41+
}
42+
}

modules/nextflow/src/main/groovy/nextflow/secret/SecretsLoader.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,9 @@ class SecretsLoader {
7878
final provider = isEnabled() ? getInstance().load() : new NullProvider()
7979
return makeSecretsContext(provider)
8080
}
81-
81+
82+
static Object secretContext(SecretsProvider provider) {
83+
return makeSecretsContext(provider)
84+
}
85+
8286
}

modules/nextflow/src/main/groovy/nextflow/trace/TraceObserverFactoryV2.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import org.pf4j.ExtensionPoint
2121
/**
2222
* Factory class for creating {@link TraceObserverV2} instances
2323
*
24-
* @author Ben Shermann <bentshermann@gmail.com>
24+
* @author Ben Sherman <bentshermann@gmail.com>
2525
*/
2626
interface TraceObserverFactoryV2 extends ExtensionPoint {
2727

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
set -e
3+
4+
$NXF_CMD secrets set MY_SECRET hello-world
5+
6+
$NXF_RUN -c ../../config-secrets.config | tee stdout
7+
8+
< .nextflow.log grep "Config file used secrets -- reloading config with secrets provider" || false
9+
< stdout grep "outputDir: results-hello-world" || false
10+
11+
$NXF_CMD secrets delete MY_SECRET

tests/config-secrets.config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
outputDir = secrets.MY_SECRET ? "results-$secrets.MY_SECRET" : 'results'

tests/config-secrets.nf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
workflow {
3+
println "outputDir: ${workflow.outputDir.name}"
4+
}

0 commit comments

Comments
 (0)