Security validator - Pulkit Sharma's GSoC project
This commit is contained in:
commit
dfae860ca8
13
Makefile
13
Makefile
|
|
@ -96,6 +96,7 @@ e2e: deepcopy-gen manifests ## Runs e2e tests, you can use EXTRA_ARGS
|
|||
|
||||
.PHONY: helm-e2e
|
||||
IMAGE_NAME := $(DOCKER_REGISTRY):$(GITCOMMIT)
|
||||
|
||||
helm-e2e: helm container-runtime-build ## Runs helm e2e tests, you can use EXTRA_ARGS
|
||||
@echo "+ $@"
|
||||
RUNNING_TESTS=1 go test -parallel=1 "./test/helm/" -ginkgo.v -tags "$(BUILDTAGS) cgo" -v -timeout 60m -run "$(E2E_TEST_SELECTOR)" -image-name=$(IMAGE_NAME) $(E2E_TEST_ARGS)
|
||||
|
|
@ -517,3 +518,15 @@ kubebuilder:
|
|||
mkdir -p ${ENVTEST_ASSETS_DIR}
|
||||
test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh
|
||||
source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR);
|
||||
|
||||
# install cert-manager v1.5.1
|
||||
install-cert-manager: minikube-start
|
||||
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.1/cert-manager.yaml
|
||||
|
||||
uninstall-cert-manager: minikube-start
|
||||
kubectl delete -f https://github.com/jetstack/cert-manager/releases/download/v1.5.1/cert-manager.yaml
|
||||
|
||||
# Deploy the operator locally along with webhook using helm charts
|
||||
deploy-webhook: container-runtime-build
|
||||
@echo "+ $@"
|
||||
bin/helm upgrade jenkins chart/jenkins-operator --install --set-string operator.image=${IMAGE_NAME} --set webhook.enabled=true --set jenkins.enabled=false
|
||||
|
|
|
|||
1
PROJECT
1
PROJECT
|
|
@ -7,6 +7,7 @@ resources:
|
|||
group: jenkins.io
|
||||
kind: Jenkins
|
||||
version: v1alpha2
|
||||
webhookVersion: v1
|
||||
version: 3-alpha
|
||||
plugins:
|
||||
manifests.sdk.operatorframework.io/v2: {}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ type JenkinsSpec struct {
|
|||
// +optional
|
||||
SeedJobs []SeedJob `json:"seedJobs,omitempty"`
|
||||
|
||||
// ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks.
|
||||
//+optional
|
||||
ValidateSecurityWarnings bool `json:"ValidateSecurityWarnings,omitempty"`
|
||||
|
||||
// Notifications defines list of a services which are used to inform about Jenkins status
|
||||
// Can be used to integrate chat services like Slack, Microsoft Teams or Mailgun
|
||||
// +optional
|
||||
|
|
|
|||
|
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
Copyright 2021.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/log"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/plugins"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
)
|
||||
|
||||
var (
|
||||
jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package.
|
||||
PluginsMgr PluginDataManager = *NewPluginsDataManager("/tmp/plugins.json.gzip", "/tmp/plugins.json", false, time.Duration(1000)*time.Second)
|
||||
_ webhook.Validator = &Jenkins{}
|
||||
)
|
||||
|
||||
const Hosturl = "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip"
|
||||
|
||||
func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewWebhookManagedBy(mgr).
|
||||
For(in).
|
||||
Complete()
|
||||
}
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
|
||||
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
|
||||
// +kubebuilder:webhook:path=/validate-jenkins-io-jenkins-io-v1alpha2-jenkins,mutating=false,failurePolicy=fail,sideEffects=None,groups=jenkins.io.jenkins.io,resources=jenkins,verbs=create;update,versions=v1alpha2,name=vjenkins.kb.io,admissionReviewVersions={v1,v1beta1}
|
||||
|
||||
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
||||
func (in *Jenkins) ValidateCreate() error {
|
||||
if in.Spec.ValidateSecurityWarnings {
|
||||
jenkinslog.Info("validate create", "name", in.Name)
|
||||
return Validate(*in)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
||||
func (in *Jenkins) ValidateUpdate(old runtime.Object) error {
|
||||
if in.Spec.ValidateSecurityWarnings {
|
||||
jenkinslog.Info("validate update", "name", in.Name)
|
||||
return Validate(*in)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (in *Jenkins) ValidateDelete() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type PluginDataManager struct {
|
||||
PluginDataCache PluginsInfo
|
||||
Timeout time.Duration
|
||||
CompressedFilePath string
|
||||
PluginDataFile string
|
||||
IsCached bool
|
||||
Attempts int
|
||||
SleepTime time.Duration
|
||||
}
|
||||
|
||||
type PluginsInfo struct {
|
||||
Plugins []PluginInfo `json:"plugins"`
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
SecurityWarnings []Warning `json:"securityWarnings"`
|
||||
}
|
||||
|
||||
type Warning struct {
|
||||
Versions []Version `json:"versions"`
|
||||
ID string `json:"id"`
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
FirstVersion string `json:"firstVersion"`
|
||||
LastVersion string `json:"lastVersion"`
|
||||
}
|
||||
|
||||
type PluginData struct {
|
||||
Version string
|
||||
Kind string
|
||||
}
|
||||
|
||||
// Validates security warnings for both updating and creating a Jenkins CR
|
||||
func Validate(r Jenkins) error {
|
||||
if !PluginsMgr.IsCached {
|
||||
return errors.New("plugins data has not been fetched")
|
||||
}
|
||||
|
||||
pluginSet := make(map[string]PluginData)
|
||||
var faultyBasePlugins string
|
||||
var faultyUserPlugins string
|
||||
basePlugins := plugins.BasePlugins()
|
||||
|
||||
for _, plugin := range basePlugins {
|
||||
// Only Update the map if the plugin is not present or a lower version is being used
|
||||
if pluginData, ispresent := pluginSet[plugin.Name]; !ispresent || semver.Compare(makeSemanticVersion(plugin.Version), pluginData.Version) == 1 {
|
||||
pluginSet[plugin.Name] = PluginData{Version: plugin.Version, Kind: "base"}
|
||||
}
|
||||
}
|
||||
|
||||
for _, plugin := range r.Spec.Master.Plugins {
|
||||
if pluginData, ispresent := pluginSet[plugin.Name]; !ispresent || semver.Compare(makeSemanticVersion(plugin.Version), pluginData.Version) == 1 {
|
||||
pluginSet[plugin.Name] = PluginData{Version: plugin.Version, Kind: "user-defined"}
|
||||
}
|
||||
}
|
||||
|
||||
for _, plugin := range PluginsMgr.PluginDataCache.Plugins {
|
||||
if pluginData, ispresent := pluginSet[plugin.Name]; ispresent {
|
||||
var hasVulnerabilities bool
|
||||
for _, warning := range plugin.SecurityWarnings {
|
||||
for _, version := range warning.Versions {
|
||||
firstVersion := version.FirstVersion
|
||||
lastVersion := version.LastVersion
|
||||
if len(firstVersion) == 0 {
|
||||
firstVersion = "0" // setting default value in case of empty string
|
||||
}
|
||||
if len(lastVersion) == 0 {
|
||||
lastVersion = pluginData.Version // setting default value in case of empty string
|
||||
}
|
||||
// checking if this warning applies to our version as well
|
||||
if compareVersions(firstVersion, lastVersion, pluginData.Version) {
|
||||
jenkinslog.Info("Security Vulnerability detected in "+pluginData.Kind+" "+plugin.Name+":"+pluginData.Version, "Warning message", warning.Message, "For more details,check security advisory", warning.URL)
|
||||
hasVulnerabilities = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasVulnerabilities {
|
||||
if pluginData.Kind == "base" {
|
||||
faultyBasePlugins += "\n" + plugin.Name + ":" + pluginData.Version
|
||||
} else {
|
||||
faultyUserPlugins += "\n" + plugin.Name + ":" + pluginData.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(faultyBasePlugins) > 0 || len(faultyUserPlugins) > 0 {
|
||||
var errormsg string
|
||||
if len(faultyBasePlugins) > 0 {
|
||||
errormsg += "security vulnerabilities detected in the following base plugins: " + faultyBasePlugins
|
||||
}
|
||||
if len(faultyUserPlugins) > 0 {
|
||||
errormsg += "security vulnerabilities detected in the following user-defined plugins: " + faultyUserPlugins
|
||||
}
|
||||
return errors.New(errormsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPluginsDataManager(compressedFilePath string, pluginDataFile string, isCached bool, timeout time.Duration) *PluginDataManager {
|
||||
return &PluginDataManager{
|
||||
CompressedFilePath: compressedFilePath,
|
||||
PluginDataFile: pluginDataFile,
|
||||
IsCached: isCached,
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (in *PluginDataManager) ManagePluginData(sig chan bool) {
|
||||
var isInit bool
|
||||
var retryInterval time.Duration
|
||||
for {
|
||||
var isCached bool
|
||||
err := in.fetchPluginData()
|
||||
if err == nil {
|
||||
isCached = true
|
||||
} else {
|
||||
jenkinslog.Info("Cache plugin data", "failed to fetch plugin data", err)
|
||||
}
|
||||
// should only be executed once when the operator starts
|
||||
if !isInit {
|
||||
sig <- isCached // sending signal to main to continue
|
||||
isInit = true
|
||||
}
|
||||
|
||||
in.IsCached = in.IsCached || isCached
|
||||
if !isCached {
|
||||
retryInterval = time.Duration(1) * time.Hour
|
||||
} else {
|
||||
retryInterval = time.Duration(12) * time.Hour
|
||||
}
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// Downloads extracts and reads the JSON data in every 12 hours
|
||||
func (in *PluginDataManager) fetchPluginData() error {
|
||||
jenkinslog.Info("Initializing/Updating the plugin data cache")
|
||||
var err error
|
||||
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ {
|
||||
err = in.download()
|
||||
if err != nil {
|
||||
jenkinslog.V(log.VDebug).Info("Cache Plugin Data", "failed to download file", err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ {
|
||||
err = in.extract()
|
||||
if err != nil {
|
||||
jenkinslog.V(log.VDebug).Info("Cache Plugin Data", "failed to extract file", err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ {
|
||||
err = in.cache()
|
||||
if err != nil {
|
||||
jenkinslog.V(log.VDebug).Info("Cache Plugin Data", "failed to read plugin data file", err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (in *PluginDataManager) download() error {
|
||||
out, err := os.Create(in.CompressedFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
client := http.Client{
|
||||
Timeout: in.Timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(Hosturl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (in *PluginDataManager) extract() error {
|
||||
reader, err := os.Open(in.CompressedFilePath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
archive, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer archive.Close()
|
||||
writer, err := os.Create(in.PluginDataFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
_, err = io.Copy(writer, archive)
|
||||
return err
|
||||
}
|
||||
|
||||
// Loads the JSON data into memory and stores it
|
||||
func (in *PluginDataManager) cache() error {
|
||||
jsonFile, err := os.Open(in.PluginDataFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer jsonFile.Close()
|
||||
byteValue, err := ioutil.ReadAll(jsonFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(byteValue, &in.PluginDataCache)
|
||||
return err
|
||||
}
|
||||
|
||||
// returns a semantic version that can be used for comparison, allowed versioning format vMAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH
|
||||
func makeSemanticVersion(version string) string {
|
||||
if version[0] != 'v' {
|
||||
version = "v" + version
|
||||
}
|
||||
return semver.Canonical(version)
|
||||
}
|
||||
|
||||
// Compare if the current version lies between first version and last version
|
||||
func compareVersions(firstVersion string, lastVersion string, pluginVersion string) bool {
|
||||
firstSemVer := makeSemanticVersion(firstVersion)
|
||||
lastSemVer := makeSemanticVersion(lastVersion)
|
||||
pluginSemVer := makeSemanticVersion(pluginVersion)
|
||||
if semver.Compare(pluginSemVer, firstSemVer) == -1 || semver.Compare(pluginSemVer, lastSemVer) == 1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package v1alpha2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestMakeSemanticVersion(t *testing.T) {
|
||||
t.Run("only major version specified", func(t *testing.T) {
|
||||
got := makeSemanticVersion("1")
|
||||
assert.Equal(t, got, "v1.0.0")
|
||||
})
|
||||
|
||||
t.Run("major and minor version specified", func(t *testing.T) {
|
||||
got := makeSemanticVersion("1.2")
|
||||
assert.Equal(t, got, "v1.2.0")
|
||||
})
|
||||
|
||||
t.Run("major,minor and patch version specified", func(t *testing.T) {
|
||||
got := makeSemanticVersion("1.2.3")
|
||||
assert.Equal(t, got, "v1.2.3")
|
||||
})
|
||||
|
||||
t.Run("semantic versions begin with a leading v and no patch version", func(t *testing.T) {
|
||||
got := makeSemanticVersion("v2.5")
|
||||
assert.Equal(t, got, "v2.5.0")
|
||||
})
|
||||
|
||||
t.Run("semantic versions with prerelease versions", func(t *testing.T) {
|
||||
got := makeSemanticVersion("2.1.2-alpha.1")
|
||||
assert.Equal(t, got, "v2.1.2-alpha.1")
|
||||
})
|
||||
|
||||
t.Run("semantic versions with prerelease versions", func(t *testing.T) {
|
||||
got := makeSemanticVersion("0.11.2-9.c8b45b8bb173")
|
||||
assert.Equal(t, got, "v0.11.2-9.c8b45b8bb173")
|
||||
})
|
||||
|
||||
t.Run("semantic versions with build suffix", func(t *testing.T) {
|
||||
got := makeSemanticVersion("1.7.9+meta")
|
||||
assert.Equal(t, got, "v1.7.9")
|
||||
})
|
||||
|
||||
t.Run("invalid semantic version", func(t *testing.T) {
|
||||
got := makeSemanticVersion("google-login-1.2")
|
||||
assert.Equal(t, got, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
t.Run("Plugin Version lies between first and last version", func(t *testing.T) {
|
||||
got := compareVersions("1.2", "1.6", "1.4")
|
||||
assert.Equal(t, got, true)
|
||||
})
|
||||
t.Run("Plugin Version is greater than the last version", func(t *testing.T) {
|
||||
got := compareVersions("1", "2", "3")
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
t.Run("Plugin Version is less than the first version", func(t *testing.T) {
|
||||
got := compareVersions("1.4", "2.5", "1.1")
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
|
||||
t.Run("Plugins Versions have prerelease version and it lies between first and last version", func(t *testing.T) {
|
||||
got := compareVersions("1.2.1-alpha", "1.2.1", "1.2.1-beta")
|
||||
assert.Equal(t, got, true)
|
||||
})
|
||||
|
||||
t.Run("Plugins Versions have prerelease version and it is greater than the last version", func(t *testing.T) {
|
||||
got := compareVersions("v2.2.1-alpha", "v2.5.1-beta.1", "v2.5.1-beta.2")
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Run("Validating when plugins data file is not fetched", func(t *testing.T) {
|
||||
userplugins := []Plugin{{Name: "script-security", Version: "1.77"}, {Name: "git-client", Version: "3.9"}, {Name: "git", Version: "4.8.1"}, {Name: "plain-credentials", Version: "1.7"}}
|
||||
jenkinscr := *createJenkinsCR(userplugins, true)
|
||||
got := jenkinscr.ValidateCreate()
|
||||
assert.Equal(t, got, errors.New("plugins data has not been fetched"))
|
||||
})
|
||||
|
||||
PluginsMgr.IsCached = true
|
||||
t.Run("Validating a Jenkins CR with plugins not having security warnings and validation is turned on", func(t *testing.T) {
|
||||
PluginsMgr.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{
|
||||
{Name: "security-script"},
|
||||
{Name: "git-client"},
|
||||
{Name: "git"},
|
||||
{Name: "google-login", SecurityWarnings: createSecurityWarnings("", "1.2")},
|
||||
{Name: "sample-plugin", SecurityWarnings: createSecurityWarnings("", "0.8")},
|
||||
{Name: "mailer"},
|
||||
{Name: "plain-credentials"}}}
|
||||
userplugins := []Plugin{{Name: "script-security", Version: "1.77"}, {Name: "git-client", Version: "3.9"}, {Name: "git", Version: "4.8.1"}, {Name: "plain-credentials", Version: "1.7"}}
|
||||
jenkinscr := *createJenkinsCR(userplugins, true)
|
||||
got := jenkinscr.ValidateCreate()
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
|
||||
t.Run("Validating a Jenkins CR with some of the plugins having security warnings and validation is turned on", func(t *testing.T) {
|
||||
PluginsMgr.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{
|
||||
{Name: "security-script", SecurityWarnings: createSecurityWarnings("1.2", "2.2")},
|
||||
{Name: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")},
|
||||
{Name: "git-client"},
|
||||
{Name: "git"},
|
||||
{Name: "sample-plugin", SecurityWarnings: createSecurityWarnings("0.8", "")},
|
||||
{Name: "command-launcher", SecurityWarnings: createSecurityWarnings("1.2", "1.4")},
|
||||
{Name: "plain-credentials"},
|
||||
{Name: "google-login", SecurityWarnings: createSecurityWarnings("1.1", "1.3")},
|
||||
{Name: "mailer", SecurityWarnings: createSecurityWarnings("1.0.3", "1.1.4")},
|
||||
}}
|
||||
userplugins := []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}}
|
||||
jenkinscr := *createJenkinsCR(userplugins, true)
|
||||
got := jenkinscr.ValidateCreate()
|
||||
assert.Equal(t, got, errors.New("security vulnerabilities detected in the following user-defined plugins: \nworkflow-cps:2.59\ngoogle-login:1.2\nmailer:1.1"))
|
||||
})
|
||||
|
||||
t.Run("Updating a Jenkins CR with some of the plugins having security warnings and validation is turned on", func(t *testing.T) {
|
||||
PluginsMgr.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{
|
||||
{Name: "handy-uri-templates-2-api", SecurityWarnings: createSecurityWarnings("2.1.8-1.0", "2.2.8-1.0")},
|
||||
{Name: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")},
|
||||
{Name: "resource-disposer", SecurityWarnings: createSecurityWarnings("0.7", "1.2")},
|
||||
{Name: "git"},
|
||||
{Name: "jjwt-api"},
|
||||
{Name: "blueocean-github-pipeline", SecurityWarnings: createSecurityWarnings("1.2.0-alpha-2", "1.2.0-beta-5")},
|
||||
{Name: "command-launcher", SecurityWarnings: createSecurityWarnings("1.2", "1.4")},
|
||||
{Name: "plain-credentials"},
|
||||
{Name: "ghprb", SecurityWarnings: createSecurityWarnings("1.1", "1.43")},
|
||||
{Name: "mailer", SecurityWarnings: createSecurityWarnings("1.0.3", "1.1.4")},
|
||||
}}
|
||||
|
||||
userplugins := []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}}
|
||||
oldjenkinscr := *createJenkinsCR(userplugins, true)
|
||||
|
||||
userplugins = []Plugin{{Name: "handy-uri-templates-2-api", Version: "2.1.8-1.0"}, {Name: "resource-disposer", Version: "0.8"}, {Name: "jjwt-api", Version: "0.11.2-9.c8b45b8bb173"}, {Name: "blueocean-github-pipeline", Version: "1.2.0-beta-3"}, {Name: "ghprb", Version: "1.39"}}
|
||||
newjenkinscr := *createJenkinsCR(userplugins, true)
|
||||
got := newjenkinscr.ValidateUpdate(&oldjenkinscr)
|
||||
assert.Equal(t, got, errors.New("security vulnerabilities detected in the following user-defined plugins: \nhandy-uri-templates-2-api:2.1.8-1.0\nresource-disposer:0.8\nblueocean-github-pipeline:1.2.0-beta-3\nghprb:1.39"))
|
||||
})
|
||||
|
||||
t.Run("Validation is turned off", func(t *testing.T) {
|
||||
userplugins := []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}}
|
||||
jenkinscr := *createJenkinsCR(userplugins, false)
|
||||
got := jenkinscr.ValidateCreate()
|
||||
assert.Nil(t, got)
|
||||
|
||||
userplugins = []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}}
|
||||
newjenkinscr := *createJenkinsCR(userplugins, false)
|
||||
got = newjenkinscr.ValidateUpdate(&jenkinscr)
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func createJenkinsCR(userPlugins []Plugin, validateSecurityWarnings bool) *Jenkins {
|
||||
jenkins := &Jenkins{
|
||||
TypeMeta: JenkinsTypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "jenkins",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: JenkinsSpec{
|
||||
Master: JenkinsMaster{
|
||||
Plugins: userPlugins,
|
||||
DisableCSRFProtection: false,
|
||||
},
|
||||
ValidateSecurityWarnings: validateSecurityWarnings,
|
||||
},
|
||||
}
|
||||
|
||||
return jenkins
|
||||
}
|
||||
|
||||
func createSecurityWarnings(firstVersion string, lastVersion string) []Warning {
|
||||
return []Warning{{Versions: []Version{{FirstVersion: firstVersion, LastVersion: lastVersion}}, ID: "null", Message: "unit testing", URL: "null", Active: false}}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
dependencies:
|
||||
- name: cert-manager
|
||||
repository: https://charts.jetstack.io
|
||||
version: v1.5.1
|
||||
digest: sha256:3220f5584bd04a8c8d4b2a076d49cc046211a463bb9a12ebbbae752be9b70bb1
|
||||
generated: "2021-08-18T01:07:49.505353718+05:30"
|
||||
|
|
@ -4,3 +4,8 @@ description: Kubernetes native operator which fully manages Jenkins on Kubernete
|
|||
name: jenkins-operator
|
||||
version: 0.5.3
|
||||
icon: https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/master/assets/jenkins-operator-icon.png
|
||||
dependencies:
|
||||
- name: cert-manager
|
||||
version: "1.5.1"
|
||||
condition: webhook.enabled
|
||||
repository: https://charts.jetstack.io
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,7 @@ kind: CustomResourceDefinition
|
|||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.1
|
||||
creationTimestamp: null
|
||||
name: jenkins.jenkins.io
|
||||
spec:
|
||||
group: jenkins.io
|
||||
|
|
@ -35,6 +36,10 @@ spec:
|
|||
spec:
|
||||
description: Spec defines the desired state of the Jenkins
|
||||
properties:
|
||||
ValidateSecurityWarnings:
|
||||
description: ValidateSecurityWarnings enables or disables validating
|
||||
potential security warnings in Jenkins plugins via admission webhooks.
|
||||
type: boolean
|
||||
backup:
|
||||
description: 'Backup defines configuration of Jenkins backup More
|
||||
info: https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configure-backup-and-restore/'
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ spec:
|
|||
securityContext:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
ValidateSecurityWarnings: {{ .Values.jenkins.ValidateSecurityWarnings }}
|
||||
{{- with .Values.jenkins.seedJobs }}
|
||||
seedJobs: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,16 @@ spec:
|
|||
protocol: TCP
|
||||
command:
|
||||
- /manager
|
||||
args: []
|
||||
args:
|
||||
{{- if .Values.webhook.enabled }}
|
||||
- --validate-security-warnings
|
||||
{{- end }}
|
||||
{{- if .Values.webhook.enabled }}
|
||||
volumeMounts:
|
||||
- mountPath: /tmp/k8s-webhook-server/serving-certs
|
||||
name: webhook-certs
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
env:
|
||||
- name: WATCH_NAMESPACE
|
||||
value: {{ .Values.jenkins.namespace }}
|
||||
|
|
@ -55,3 +64,11 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.webhook.enabled }}
|
||||
volumes:
|
||||
- name: webhook-certs
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: jenkins-{{ .Values.webhook.certificate.name }}
|
||||
terminationGracePeriodSeconds: 10
|
||||
{{- end }}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{{- if .Values.webhook.enabled }}
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: jenkins-{{ .Values.webhook.certificate.name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
duration: {{ .Values.webhook.certificate.duration }}
|
||||
renewBefore: {{ .Values.webhook.certificate.renewbefore }}
|
||||
secretName: jenkins-{{ .Values.webhook.certificate.name }}
|
||||
dnsNames:
|
||||
- jenkins-webhook-service.{{ .Release.Namespace }}.svc
|
||||
- jenkins-webhook-service.{{ .Release.Namespace }}.svc.cluster.local
|
||||
issuerRef:
|
||||
kind: Issuer
|
||||
name: selfsigned
|
||||
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: selfsigned
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
selfSigned: {}
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: jenkins-{{ .Values.webhook.certificate.name }}
|
||||
type: opaque
|
||||
|
||||
{{- end }}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{{- if .Values.webhook.enabled }}
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-webhook
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/jenkins-{{ .Values.webhook.certificate.name }}
|
||||
webhooks:
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: jenkins-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
path: /validate-jenkins-io-v1alpha2-jenkins
|
||||
failurePolicy: Fail
|
||||
name: vjenkins.kb.io
|
||||
timeoutSeconds: 30
|
||||
rules:
|
||||
- apiGroups:
|
||||
- jenkins.io
|
||||
apiVersions:
|
||||
- v1alpha2
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- jenkins
|
||||
scope: "Namespaced"
|
||||
sideEffects: None
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jenkins-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 9443
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "jenkins-operator.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
---
|
||||
{{- end }}
|
||||
|
|
@ -47,6 +47,10 @@ jenkins:
|
|||
# See https://github.com/jenkinsci/kubernetes-operator/pull/193 for more info
|
||||
disableCSRFProtection: false
|
||||
|
||||
|
||||
# ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks.
|
||||
ValidateSecurityWarnings: false
|
||||
|
||||
# imagePullSecrets is used if you want to pull images from private repository
|
||||
# See https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configuration/#pulling-docker-images-from-private-repositories for more info
|
||||
imagePullSecrets: []
|
||||
|
|
@ -277,3 +281,22 @@ operator:
|
|||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
webhook:
|
||||
# TLS certificates for webhook
|
||||
certificate:
|
||||
name: webhook-certificate
|
||||
|
||||
# validity of the certificate
|
||||
duration: 2160h
|
||||
|
||||
# time after which the certificate will be automatically renewed
|
||||
renewbefore: 360h
|
||||
# enable or disable the validation webhook
|
||||
enabled: false
|
||||
|
||||
# This startupapicheck is a Helm post-install hook that waits for the webhook
|
||||
# endpoints to become available.
|
||||
cert-manager:
|
||||
startupapicheck:
|
||||
enabled: false
|
||||
|
|
@ -5,6 +5,7 @@ kind: CustomResourceDefinition
|
|||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.1
|
||||
creationTimestamp: null
|
||||
name: jenkins.jenkins.io
|
||||
spec:
|
||||
group: jenkins.io
|
||||
|
|
@ -35,6 +36,10 @@ spec:
|
|||
spec:
|
||||
description: Spec defines the desired state of the Jenkins
|
||||
properties:
|
||||
ValidateSecurityWarnings:
|
||||
description: ValidateSecurityWarnings enables or disables validating
|
||||
potential security warnings in Jenkins plugins via admission webhooks.
|
||||
type: boolean
|
||||
backup:
|
||||
description: 'Backup defines configuration of Jenkins backup More
|
||||
info: https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configure-backup-and-restore/'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
apiVersion: jenkins.io/v1alpha2
|
||||
kind: Jenkins
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: validating-webhook-configuration
|
||||
webhooks:
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
path: /validate-jenkins-io-jenkins-io-v1alpha2-jenkins
|
||||
failurePolicy: Fail
|
||||
name: vjenkins.kb.io
|
||||
rules:
|
||||
- apiGroups:
|
||||
- jenkins.io.jenkins.io
|
||||
apiVersions:
|
||||
- v1alpha2
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- jenkins
|
||||
sideEffects: None
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 9443
|
||||
selector:
|
||||
control-plane: controller-manager
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: jenkins-webhook-certificate
|
||||
namespace: default
|
||||
spec:
|
||||
duration: 2160h
|
||||
renewBefore: 360h
|
||||
secretName: jenkins-webhook-certificate
|
||||
dnsNames:
|
||||
- jenkins-webhook-service.default.svc
|
||||
- jenkins-webhook-service.default.svc.cluster.local
|
||||
issuerRef:
|
||||
kind: Issuer
|
||||
name: selfsigned
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: selfsigned
|
||||
namespace: default
|
||||
spec:
|
||||
selfSigned: {}
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: jenkins-webhook
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: default/jenkins-webhook-certificate
|
||||
webhooks:
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: jenkins-webhook-service
|
||||
namespace: default
|
||||
path: /validate-jenkins-io-v1alpha2-jenkins
|
||||
failurePolicy: Fail
|
||||
name: vjenkins.kb.io
|
||||
timeoutSeconds: 30
|
||||
rules:
|
||||
- apiGroups:
|
||||
- jenkins.io
|
||||
apiVersions:
|
||||
- v1alpha2
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- jenkins
|
||||
scope: "Namespaced"
|
||||
sideEffects: None
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jenkins-webhook-service
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 9443
|
||||
selector:
|
||||
control-plane: controller-manager
|
||||
---
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: jenkins-webhook-certificate
|
||||
namespace: default
|
||||
spec:
|
||||
duration: 2160h
|
||||
renewBefore: 360h
|
||||
secretName: jenkins-webhook-certificate
|
||||
dnsNames:
|
||||
- jenkins-webhook-service.default.svc
|
||||
- jenkins-webhook-service.default.svc.cluster.local
|
||||
issuerRef:
|
||||
kind: Issuer
|
||||
name: selfsigned
|
||||
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: selfsigned
|
||||
namespace: default
|
||||
spec:
|
||||
selfSigned: {}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: jenkins-webhook
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: default/jenkins-webhook-certificate
|
||||
webhooks:
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: jenkins-webhook-service
|
||||
namespace: default
|
||||
path: /validate-jenkins-io-v1alpha2-jenkins
|
||||
failurePolicy: Fail
|
||||
name: vjenkins.kb.io
|
||||
timeoutSeconds: 30
|
||||
rules:
|
||||
- apiGroups:
|
||||
- jenkins.io
|
||||
apiVersions:
|
||||
- v1alpha2
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- jenkins
|
||||
scope: "Namespaced"
|
||||
sideEffects: None
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jenkins-webhook-service
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 9443
|
||||
selector:
|
||||
name: jenkins-operator
|
||||
---
|
||||
|
||||
2
go.mod
2
go.mod
|
|
@ -27,4 +27,6 @@ require (
|
|||
k8s.io/client-go v0.20.2
|
||||
k8s.io/utils v0.0.0-20201110183641-67b214c5f920
|
||||
sigs.k8s.io/controller-runtime v0.7.0
|
||||
golang.org/x/mod v0.4.2
|
||||
|
||||
)
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -572,6 +572,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
|
|||
16
main.go
16
main.go
|
|
@ -78,6 +78,7 @@ func main() {
|
|||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var ValidateSecurityWarnings bool
|
||||
|
||||
isRunningInCluster, err := resources.IsRunningInCluster()
|
||||
if err != nil {
|
||||
|
|
@ -88,6 +89,7 @@ func main() {
|
|||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", isRunningInCluster, "Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&ValidateSecurityWarnings, "validate-security-warnings", false, "Enable validation for potential security warnings in jenkins custom resource plugins")
|
||||
hostname := flag.String("jenkins-api-hostname", "", "Hostname or IP of Jenkins API. It can be service name, node IP or localhost.")
|
||||
port := flag.Int("jenkins-api-port", 0, "The port on which Jenkins API is running. Note: If you want to use nodePort don't set this setting and --jenkins-api-use-nodeport must be true.")
|
||||
useNodePort := flag.Bool("jenkins-api-use-nodeport", false, "Connect to Jenkins API using the service nodePort instead of service port. If you want to set this as true - don't set --jenkins-api-port.")
|
||||
|
|
@ -109,6 +111,15 @@ func main() {
|
|||
}
|
||||
logger.Info(fmt.Sprintf("Watch namespace: %v", namespace))
|
||||
|
||||
if ValidateSecurityWarnings {
|
||||
isInitialized := make(chan bool)
|
||||
go v1alpha2.PluginsMgr.ManagePluginData(isInitialized)
|
||||
|
||||
if !<-isInitialized {
|
||||
logger.Info("Unable to get the plugins data")
|
||||
}
|
||||
}
|
||||
|
||||
// get a config to talk to the API server
|
||||
cfg, err := config.GetConfig()
|
||||
if err != nil {
|
||||
|
|
@ -169,6 +180,11 @@ func main() {
|
|||
fatal(errors.Wrap(err, "unable to create Jenkins controller"), *debug)
|
||||
}
|
||||
|
||||
if ValidateSecurityWarnings {
|
||||
if err = (&v1alpha2.Jenkins{}).SetupWebhookWithManager(mgr); err != nil {
|
||||
fatal(errors.Wrap(err, "unable to create Webhook"), *debug)
|
||||
}
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/api/v1alpha2"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/constants"
|
||||
"github.com/jenkinsci/kubernetes-operator/test/e2e"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var _ = Describe("Jenkins controller", func() {
|
||||
var _ = Describe("Jenkins Controller with webhook", func() {
|
||||
|
||||
var (
|
||||
namespace *corev1.Namespace
|
||||
)
|
||||
|
|
@ -23,12 +29,16 @@ var _ = Describe("Jenkins controller", func() {
|
|||
namespace = e2e.CreateNamespace()
|
||||
})
|
||||
AfterEach(func() {
|
||||
cmd := exec.Command("../../bin/helm", "delete", "jenkins", "--namespace", namespace.Name)
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).NotTo(HaveOccurred(), string(output))
|
||||
|
||||
e2e.ShowLogsIfTestHasFailed(CurrentGinkgoTestDescription().Failed, namespace.Name)
|
||||
e2e.DestroyNamespace(namespace)
|
||||
})
|
||||
Context("when deploying Helm Chart to cluster", func() {
|
||||
It("creates Jenkins instance and configures it", func() {
|
||||
|
||||
Context("Deploys jenkins operator with helm charts with default values", func() {
|
||||
It("Deploys Jenkins operator and configures default Jenkins instance", func() {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
TypeMeta: v1alpha2.JenkinsTypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
|
@ -39,22 +49,186 @@ var _ = Describe("Jenkins controller", func() {
|
|||
|
||||
cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug",
|
||||
"--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name),
|
||||
"--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install")
|
||||
"--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install", "--wait")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).NotTo(HaveOccurred(), string(output))
|
||||
|
||||
e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins)
|
||||
e2e.WaitForJenkinsUserConfigurationToComplete(jenkins)
|
||||
|
||||
cmd = exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug",
|
||||
"--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name),
|
||||
"--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install")
|
||||
output, err = cmd.CombinedOutput()
|
||||
})
|
||||
})
|
||||
|
||||
Context("Deploys jenkins operator with helm charts with validating webhook and jenkins instance disabled", func() {
|
||||
It("Deploys operator,denies creating a jenkins cr and creates jenkins cr with validation turned off", func() {
|
||||
|
||||
By("Deploying the operator along with webhook and cert-manager")
|
||||
cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug",
|
||||
"--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), "--set-string", fmt.Sprintf("operator.image=%s", *imageName),
|
||||
"--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).NotTo(HaveOccurred(), string(output))
|
||||
|
||||
By("Waiting for the operator to fetch the plugin data ")
|
||||
time.Sleep(time.Duration(200) * time.Second)
|
||||
|
||||
By("Denying a create request for a Jenkins custom resource with some plugins having security warnings and validation is turned on")
|
||||
userplugins := []v1alpha2.Plugin{
|
||||
{Name: "simple-theme-plugin", Version: "0.6"},
|
||||
{Name: "audit-trail", Version: "3.5"},
|
||||
{Name: "github", Version: "1.29.0"},
|
||||
}
|
||||
jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, true)
|
||||
Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(MatchError("admission webhook \"vjenkins.kb.io\" denied the request: security vulnerabilities detected in the following user-defined plugins: \naudit-trail:3.5\ngithub:1.29.0"))
|
||||
|
||||
By("Creating the Jenkins resource with plugins not having any security warnings and validation is turned on")
|
||||
userplugins = []v1alpha2.Plugin{
|
||||
{Name: "simple-theme-plugin", Version: "0.6"},
|
||||
{Name: "audit-trail", Version: "3.8"},
|
||||
{Name: "github", Version: "1.31.0"},
|
||||
}
|
||||
jenkins = CreateJenkinsCR("jenkins", namespace.Name, userplugins, true)
|
||||
Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed())
|
||||
e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins)
|
||||
e2e.WaitForJenkinsUserConfigurationToComplete(jenkins)
|
||||
|
||||
})
|
||||
|
||||
It("Deploys operator, creates a jenkins cr and denies update request for another one", func() {
|
||||
By("Deploying the operator along with webhook and cert-manager")
|
||||
cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug",
|
||||
"--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), "--set-string", fmt.Sprintf("operator.image=%s", *imageName),
|
||||
"--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).NotTo(HaveOccurred(), string(output))
|
||||
|
||||
By("Waiting for the operator to fetch the plugin data ")
|
||||
time.Sleep(time.Duration(200) * time.Second)
|
||||
|
||||
By("Creating a Jenkins custom resource with some plugins having security warnings but validation is turned off")
|
||||
userplugins := []v1alpha2.Plugin{
|
||||
{Name: "simple-theme-plugin", Version: "0.6"},
|
||||
{Name: "audit-trail", Version: "3.5"},
|
||||
{Name: "github", Version: "1.29.0"},
|
||||
}
|
||||
jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, false)
|
||||
Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed())
|
||||
e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins)
|
||||
e2e.WaitForJenkinsUserConfigurationToComplete(jenkins)
|
||||
|
||||
By("Failing to update the Jenkins custom resource because some plugins have security warnings and validation is turned on")
|
||||
userplugins = []v1alpha2.Plugin{
|
||||
{Name: "vncviewer", Version: "1.7"},
|
||||
{Name: "build-timestamp", Version: "1.0.3"},
|
||||
{Name: "deployit-plugin", Version: "7.5.5"},
|
||||
{Name: "github-branch-source", Version: "2.0.7"},
|
||||
{Name: "aws-lambda-cloud", Version: "0.4"},
|
||||
{Name: "groovy", Version: "1.31"},
|
||||
{Name: "google-login", Version: "1.2"},
|
||||
}
|
||||
jenkins.Spec.Master.Plugins = userplugins
|
||||
jenkins.Spec.ValidateSecurityWarnings = true
|
||||
Expect(e2e.K8sClient.Update(context.TODO(), jenkins)).Should(MatchError("admission webhook \"vjenkins.kb.io\" denied the request: security vulnerabilities detected in the following user-defined plugins: \nvncviewer:1.7\ndeployit-plugin:7.5.5\ngithub-branch-source:2.0.7\ngroovy:1.31\ngoogle-login:1.2"))
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
func CreateJenkinsCR(name string, namespace string, userPlugins []v1alpha2.Plugin, validateSecurityWarnings bool) *v1alpha2.Jenkins {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
TypeMeta: v1alpha2.JenkinsTypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
GroovyScripts: v1alpha2.GroovyScripts{
|
||||
Customization: v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{},
|
||||
Secret: v1alpha2.SecretRef{
|
||||
Name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
ConfigurationAsCode: v1alpha2.ConfigurationAsCode{
|
||||
Customization: v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{},
|
||||
Secret: v1alpha2.SecretRef{
|
||||
Name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Name: resources.JenkinsMasterContainerName,
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "TEST_ENV",
|
||||
Value: "test_env_value",
|
||||
},
|
||||
},
|
||||
ReadinessProbe: &corev1.Probe{
|
||||
Handler: corev1.Handler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/login",
|
||||
Port: intstr.FromString("http"),
|
||||
Scheme: corev1.URISchemeHTTP,
|
||||
},
|
||||
},
|
||||
InitialDelaySeconds: int32(100),
|
||||
TimeoutSeconds: int32(4),
|
||||
FailureThreshold: int32(40),
|
||||
SuccessThreshold: int32(1),
|
||||
PeriodSeconds: int32(10),
|
||||
},
|
||||
LivenessProbe: &corev1.Probe{
|
||||
Handler: corev1.Handler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/login",
|
||||
Port: intstr.FromString("http"),
|
||||
Scheme: corev1.URISchemeHTTP,
|
||||
},
|
||||
},
|
||||
InitialDelaySeconds: int32(80),
|
||||
TimeoutSeconds: int32(4),
|
||||
FailureThreshold: int32(30),
|
||||
SuccessThreshold: int32(1),
|
||||
PeriodSeconds: int32(5),
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "plugins-cache",
|
||||
MountPath: "/usr/share/jenkins/ref/plugins",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "envoyproxy",
|
||||
Image: "envoyproxy/envoy-alpine:v1.14.1",
|
||||
},
|
||||
},
|
||||
Plugins: userPlugins,
|
||||
DisableCSRFProtection: false,
|
||||
NodeSelector: map[string]string{"kubernetes.io/os": "linux"},
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "plugins-cache",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ValidateSecurityWarnings: validateSecurityWarnings,
|
||||
Service: v1alpha2.Service{
|
||||
Type: corev1.ServiceTypeNodePort,
|
||||
Port: constants.DefaultHTTPPortInt32,
|
||||
},
|
||||
JenkinsAPISettings: v1alpha2.JenkinsAPISettings{AuthorizationStrategy: v1alpha2.CreateUserAuthorizationStrategy},
|
||||
},
|
||||
}
|
||||
|
||||
return jenkins
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ kubectl --context remote-k8s --namespace default get po
|
|||
|
||||
Tests are written using [Ginkgo](https://onsi.github.io/ginkgo/) with [Gomega](https://onsi.github.io/gomega/).
|
||||
|
||||
Run unit tests with go fmt, lint, statickcheck, vet:
|
||||
Run unit tests with go fmt, lint, staticcheck, vet:
|
||||
|
||||
```bash
|
||||
make verify
|
||||
|
|
@ -262,6 +262,12 @@ make minikube-start
|
|||
make e2e
|
||||
```
|
||||
|
||||
Run Helm e2e tests:
|
||||
```bash
|
||||
eval $(bin/minikube docker-env)
|
||||
make helm-e2e
|
||||
```
|
||||
|
||||
Run the specific e2e test:
|
||||
|
||||
```bash
|
||||
|
|
@ -292,8 +298,13 @@ kubectl get secret jenkins-operator-credentials-<cr_name> -o 'jsonpath={.data.us
|
|||
kubectl get secret jenkins-operator-credentials-<cr_name> -o 'jsonpath={.data.password}' | base64 -d
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Webhook
|
||||
To deploy the operator along with webhook, run :
|
||||
```bash
|
||||
eval $(minikube docker-env)
|
||||
make deploy-webhook
|
||||
```
|
||||
It uses [cert-manager](https://cert-manager.io/) as an external dependency.
|
||||
|
||||
## Self-learning
|
||||
|
||||
|
|
@ -304,6 +315,8 @@ kubectl get secret jenkins-operator-credentials-<cr_name> -o 'jsonpath={.data.pa
|
|||
|
||||
* [Operator SDK Tutorial for Go](https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/)
|
||||
|
||||
* [Kubebuilder Validating Webhook Implementation](https://book.kubebuilder.io/cronjob-tutorial/webhook-implementation.html)
|
||||
|
||||
[dep_tool]:https://golang.github.io/dep/docs/installation.html
|
||||
[git_tool]:https://git-scm.com/downloads
|
||||
[go_tool]:https://golang.org/dl/
|
||||
|
|
@ -314,4 +327,3 @@ kubectl get secret jenkins-operator-credentials-<cr_name> -o 'jsonpath={.data.pa
|
|||
[minikube]:https://kubernetes.io/docs/tasks/tools/install-minikube/
|
||||
[virtualbox]:https://www.virtualbox.org/wiki/Downloads
|
||||
[install_dev_tools]:https://jenkinsci.github.io/kubernetes-operator/docs/developer-guide/tools/
|
||||
|
||||
|
|
|
|||
|
|
@ -893,3 +893,97 @@ below is the full list of those volumeMounts:
|
|||
* scripts
|
||||
* init-configuration
|
||||
* operator-credentials
|
||||
|
||||
## Validating Webhook
|
||||
Validating webhook can be used in order to increase the Operator's capabilities to monitor security issues. It will look for security vulnerabilities in the base and requested plugins. It can be easily installed via Helm charts by setting webhook.enabled in values.yaml.
|
||||
|
||||
|
||||
**Note**: The webhook takes some time to get up and running. It's recommended to first deploy the Operator and later Jenkins Custom Resource by using toggles in `values.yaml`.
|
||||
For the installation with yaml manifests (without using Helm chart), first, install cert-manager:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.1/cert-manager.yaml
|
||||
```
|
||||
|
||||
It takes some time to get cert-manager up and running.
|
||||
Then, install the webhook and other required resources:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/master/deploy/all-in-one-webhook.yaml
|
||||
```
|
||||
|
||||
Now, download the manifests for the operator and other resources from [here](https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/master/deploy/all-in-one-v1alpha2.yaml) and provide these additional fields in the Operator manifest:
|
||||
|
||||
<pre>
|
||||
<code>
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jenkins-operator
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
spec:
|
||||
serviceAccountName: jenkins-operator
|
||||
securityContext:
|
||||
runAsUser: 65532
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
<b>- --validate-security-warnings</b>
|
||||
image: jenkins-operator:54231733-dirty
|
||||
name: jenkins-operator
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 100Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 20Mi
|
||||
env:
|
||||
- name: WATCH_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
<b>volumeMounts:
|
||||
- mountPath: /tmp/k8s-webhook-server/serving-certs
|
||||
name: webhook-certs
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: webhook-certs
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: jenkins-webhook-certificate
|
||||
terminationGracePeriodSeconds: 10</b>
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
To enable security validation in the Jenkins Custom Resource, set
|
||||
|
||||
>jenkins.ValidateSecurityWarnings=true
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ spec:
|
|||
targets: "cicd/jobs/*.jenkins"
|
||||
description: "Jenkins Operator repository"
|
||||
repositoryBranch: master
|
||||
repositoryUrl: ssh://git@github.com:jenkinsci/kubernetes-operator.git
|
||||
repositoryUrl: git@github.com:jenkinsci/kubernetes-operator.git
|
||||
```
|
||||
|
||||
and create a Kubernetes Secret (name of secret should be the same from `credentialID` field):
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ Plugin's configuration is applied as groovy scripts or the [configuration as cod
|
|||
Any plugin working for Jenkins can be installed by the Jenkins Operator.
|
||||
|
||||
Pre-installed plugins:
|
||||
* configuration-as-code v1.47
|
||||
* git v4.5.0
|
||||
|
||||
* configuration-as-code v1.51
|
||||
* git v4.7.2
|
||||
* job-dsl v1.77
|
||||
* kubernetes-credentials-provider v0.15
|
||||
* kubernetes v1.29.2
|
||||
* kubernetes-credentials-provider v0.18-1
|
||||
* kubernetes v1.30.0
|
||||
* workflow-aggregator v2.6
|
||||
* workflow-job v2.40
|
||||
* workflow-job v2.41
|
||||
|
||||
Rest of the plugins can be found in [plugins repository](https://plugins.jenkins.io/).
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ Rest of the plugins can be found in [plugins repository](https://plugins.jenkins
|
|||
|
||||
Edit Custom Resource under `spec.master.plugins`:
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: jenkins.io/v1alpha2
|
||||
kind: Jenkins
|
||||
metadata:
|
||||
|
|
@ -51,19 +52,19 @@ spec:
|
|||
master:
|
||||
basePlugins:
|
||||
- name: kubernetes
|
||||
version: "1.29.2"
|
||||
version: "1.30.0"
|
||||
- name: workflow-job
|
||||
version: "2.40"
|
||||
- name: workflow-aggregator
|
||||
version: "2.6"
|
||||
- name: git
|
||||
version: "4.5.0"
|
||||
version: "4.7.2"
|
||||
- name: job-dsl
|
||||
version: "1.77"
|
||||
- name: configuration-as-code
|
||||
version: "1.47"
|
||||
version: "1.51"
|
||||
- name: kubernetes-credentials-provider
|
||||
version: "0.15"
|
||||
version: "0.18-1"
|
||||
```
|
||||
|
||||
You can change their versions.
|
||||
|
|
|
|||
|
|
@ -35,3 +35,7 @@ The **Jenkins Operator** design incorporates the following concepts:
|
|||
Operator state is kept in the custom resource status section, which is used for storing any configuration events or job statuses managed by the operator.
|
||||
|
||||
It helps to maintain or recover the desired state even after the operator or Jenkins restarts.
|
||||
|
||||
## Webhook
|
||||
|
||||
It rejects/accepts admission requests based on potential security warnings in plugins present in the Jenkins Custom Resource.
|
||||
|
|
|
|||
Loading…
Reference in New Issue