-
New Feature
-
Resolution: Unresolved
-
Minor
-
None
-
Powered by SuggestiMate
We are attempting to block access to pipeline steps that we consider a security risk (e.g. the Kubernetes plugin steps) from user provided Jenkinsfiles, but allowing access to all the steps from global pipeline libraries. Ideally we would only allow access to steps within our global pipeline library.
The only way we have come up with is to create StepListener extension. The extension uses reflection on the StepContext to check whether the step was executed via the Jenkinsfile or a global pipeline library.
As I understand it, this is not considered good practice and may break in future.
Is there some existing functionality to achieve what we need here or the possibility of extending the StepListener API to provide it?
[JENKINS-69606] Ability to whitelist steps in Jenkinsfiles while allowing all steps in global pipeline libraries
Added script-security-plugin as a component. This might be best a feature request in the script security plugin. To enable a vars and steps allow or block list.
mwinter69 shared with me some great resources from Gitter. I plan to implement and open source a plugin for this since this is a need I have.
Potential extension points:
Another example, shell step https://github.com/jenkinsci/workflow-durable-task-step-plugin/blob/master/src/main/java/org/jenkinsci/plugins/workflow/steps/durable_task/ShellStep.java has a getFunction call which returns "sh" on its descriptor impl.
Here's an example of sandbox usage.
https://javadoc.jenkins.io/plugin/workflow-cps/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.html CPS flow definition has isSandbox method. This could hypothetically be used to determine if a step was called by a global shared library or a user Jenkinsfile. Shared libraries are not sandboxed while user Jenkinsfiles are. I'll need to test more on this when I actually start implementing.
Step implements getDescriptor so to get the step name via Groovy one would call ".descriptor.functionName"
Global variables get invoked here. It would be nice if there was a Listener similar to StepListener to do some kind of pre-flight check before it is called.
I also notice CpsWhitelist has it. Perhaps I could update the whitelist at runtime somehow to prevent users from calling the shared library steps.
Currently, there's no easy way to determine if a step was called from a Global var (out of sandbox) or a user pipeline (in sandbox). I'm still researching.
jglick what are your thoughts on supporting adding a forbiddenMethod in script security WhiteList with an abstract default to false? And then plugins like this one could implement the ExtensionPoint and reject steps and vars via script security? It would reduce runtime load by not having to snoop with StepListener (not that there's much) but the primary benefit is this way would cover vars and steps alike.
I'm referring to "forbidden" equivalents to all methods in https://github.com/jenkinsci/script-security-plugin/blob/bc3f03b64447fbb24f8380aca2598162499060f7/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java with abstract implementations to support backwards compatibility. i.e. nothing forbidden by default but a plugin like this one could override it.
With forbidden taking precedence over permitted. i.e. an admin should be able to forbid anything regardless of built-in whitelisted syntax in the pipeline AST.
And potential implementation in ProxyWhitelist.java
boolean permitted = false; boolean forbidden = false; for (Whitelist delegate : delegates) { if (delegate.permitsMethod(method, receiver, args)) { permitted = true; } if (delegate.forbidsMethod(method, receiver, args)) { forbidden = true; } } return (forbidden) ? false : permitted;
I do not recommend messing around with Whitelist. What is wrong with StepListener? It is designed for exactly this purpose.
StepListener does not cover vars (neither built-in like "pipeline" var for model definition nor admin-provided global vars).
Use case 1
For example, I use scripted pipeline. As part of maintaining ephemeral credentials (in a global pipeline var) we make calls to Amazon STS to provide 1 hour ephemeral AWS credentials via assume role. I don't use access keys anywhere for AWS and everything is restricted to short-lived credentials. For compliance tracking users don't interact with AWS credentials step directly and instead must go through another step. I would like to be able to block users from calling some of the pipeline vars but allow others (for global shared libs I provide).
Use case 2
We also don't support pipeline model definition because we have specific ways on how an agent should be set up. To DRY up code we have a generic buildAgent step and a stageWithAgent step. New users who are familiar with Jenkins sometimes get confused when the try to use "pipeline {}" and it would be a better user experience if rather than it failing with a strange cryptic error that the get "blocked by admins" instead.
Why not declarative?
The short version is it falls far short of my company needs and I don't have the buy-in at work to do full-time Jenkins development. I use scripted pipeline and some custom plugins that I wrote to fill the gaps but ideally I would like to contribute most of this work back upstream where possible through an agreement with my employer. As of now, I don't have that.
The experience is still decent for users. The DSL I provide to users.
pullRequestPipeline { stageWithAgent(name: 'My stage') { runSh './.ci/script.sh' } } pullRequestAndTag { publishToNexus() } branchPipeline { createTagOnMergeTo(branch: 'master') } tagPipeline { deployStage(name: 'Some deploy', environment: 'dev') { runSh './.ci/deploy.sh' } // and for staging and prod }
Surrounding this code we add before and after required stages such as compliance checking, security scanning, and other purposes. We have custom plugins extending Jenkins for capturing our logs in a particular way we need for compliance.
Challenge
Vars (and builtin vars like "pipeline {}" provided by declarative) are automatically approved in the Whitelist. This makes sense. However, when calling vars there's no step listener event fired because it's not a part of the FlowNodes in the Graph. Instead, it is called directly via invoke and bypasses the graph entirely.
See also
My desired end-state for this plugin in the Known Gaps section https://github.com/jenkinsci/restrict-steps-plugin/tree/b9ad73d82bb6af03d20f0e747f129e3652c4020f#known-gaps
Workflow CPS does not currently provide the flexibility so it needs to be patched. Either by Whitelist (for sandboxed which I think is most desirable since that runs only once) or invoke for global pipeline vars needs to notify StepListener. This makes less sense to me because they aren't steps.
This plugin is now hosted at https://github.com/jenkinsci/restrict-steps-plugin
I think we could add a new method to StepListener for global variables.
StepListener itself is currently lacking a few things. It makes a difference between a global shared libary calling a step (or var) and an end-user from sandboxed Jenkinsfile.
Currently, if you block a step with StepListener it will block it for both users and global shared pipelines. This isn't really the desired result, though it does allow me to block some steps.
Any ideas for how to detect within StepListener if someone is calling that step from a non-sandboxed global lib as opposed to a sandboxed Jenkinsfile?
Ideally, the admin libs wouldn't be blocked from using steps but end-user Jenkinsfiles would. The current implementation of restrict-steps-plugin even blocks global shared libs from calling those steps (it even blocks declarative).
But ideally, optionally including admin vars and blocking only within the scope of the Jenkinsfile would be more desirable.
Can https://github.com/jenkinsci/script-security-plugin/blob/master/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptor.java be extended to enable plugins to provide block lists? For example, a plugin blocking certain vars or steps.
I doubt Pipeline can easily be made to enforce the sort of EDSL you describe. (Nor can I recommend JTE, which has serious design issues and makes use of Pipeline internals that are not guaranteed to be stable at all.) I would rather advise not offering Pipeline Groovy directly to users at all; instead load some list of requested “steps” for example from a YAML file, and essentially interpret it as a plain DSL.
To clarify, I'm not trying to enforce the DSL and am fine with users making use of most of the Jenkins steps.
There's a few vars which pose issues if users use them directly due to what they do in the runtime and are only meant for reuse in other vars. Currently Jenkins doesn't have a concept of private vars which is essentially what I want.
The other idea was to rely on sandbox to prevent var usage but it doesn't have the infrastructure set up in it. I've considered a YAML only spec but there's a lot of benefits to letting users do some free form pipeline. The DSL I recommend is just that; a recommendation with exception for the vars I wish could be private scope.
If you do not want a variable to be used, then why declare it as a variable at all? Make it a utility inside some class.
// vars/supported1.groovy def call() { my.lib.Impl.supported1() } // vars/supported2.groovy def call() { my.lib.Impl.supported2() } // src/my/lib/Impl.groovy package my.lib public class Impl { public static void supported1() { internal('one') } public static void supported2() { internal('two') } private static void internal(String generalValuesForbidden) { // … } }
Now if this is a trusted global library and security is your concern rather than merely enforcing best practices by coworkers operating in good faith, beware that Groovy basically ignores Java access control and might let a malicious pipeline directly call Impl.internal.
I have issues with Shared pipeline src directory.
- It violates standard conventions of Java (typically set by Maven); for example it has no src/main or src/test just src. It should not have adopted existing conventions and violated them.
- It is CPS groovy, like vars, meaning all of the strangeness and restrictions of CPS groovy apply without the NonCPS annotation.
- Utilities in src are still accessible by users. Functionally, there's zero difference in a user's ability to use the code in src vs var and so at best it is conceptually organized but not functionally or restrictively separate.
I need code to be reusable across multiple vars. However, if it is reusable across multiple vars then end users are also able to access it. This was discovered through practice and attempting to self-exploit via own Jenkinsfiles. The only truly effective way is to not make the code available in shared libraries at all and instead package a binary Jar as a one-off custom library-only plugin.
By the way, I have a library-only custom Jenkins plugin already but there's still the gap and need of quickly extensible but private from end-user Jenkinsfile code necessary.
In Groovy, even if you explicitly declare a private; it is public
Now if this is a trusted global library and security is your concern
rather than merely enforcing best practices by coworkers operating in
good faith, beware that Groovy basically ignores Java access control and
might let a malicious pipeline directly call Impl.internal.
I forgot to mention, this is a global shared library and security is a concern. It's not about coworkers operating in good faith. Once you have over 1000 Jenkins users (heck even over 100) you need to start operating Jenkins like it is a public service on the internet (even though it is private).
In my case, there needs to be a concept of global shared library steps that are available only to global shared libraries and not available to user Jenkinsfiles. It's not really sustainable to offer plugins in all scenarios because even with plugins you might want to call them but not allow user Jenkinsfiles to call them.
I think extending script security would be a better design for this; enabling block lists for pipeline steps. User Jenkinsfile code is subject to sandbox where global shared pipeline libraries are not.
This just sounds like it is outside the design space for Pipeline. If you do not want users freely writing Pipeline script, then you need to deny them the ability to write Pipeline script at all, and only offer templates with predefined parameters, or some sort of (non-embedded) DSL.
This would be a useful open source plugin. Adding on, I would also like to not even necessarily allow all Vars or shared library steps. In some cases, I develop a global step meant only for private use in the global shared pipeline library. Not meant for use by users.
Detecting a global var
Here's some code which allows a dynamic var look up to see if a step is a core Jenkins step or a shared pipeline var.
https://github.com/samrocketman/jervis/blob/64d21aff1d08ae58cc310cae5e868a4923b4bcab/vars/hasGlobalVar.groovy#L33
It would be great if the whitelist could also apply to global vars or have an option around global vars.
Other necessary considerations
I've found doing a basic AST review of a Jenkinsfile is not enough because a user can pull in a user shared library via the library step. I think user shared libraries should be supported so allowing steps in a user shared library is useful to be automatically white listed without having to explicitly state allowed steps.
This also means it is necessary for limitations around allowed steps also be applied to the contents of user shared libraries.