diff --git a/Jenkins/Dockerfile b/Jenkins/Dockerfile index e4f70c9a..4ef1f7cc 100644 --- a/Jenkins/Dockerfile +++ b/Jenkins/Dockerfile @@ -5,11 +5,18 @@ USER root # Docker CLI installieren RUN apt-get update && apt-get install -y docker.io && usermod -aG docker jenkins +# Create workspace directory with correct owner and permissions +RUN mkdir -p /var/jenkins_home/workspace && \ + chown -R jenkins:jenkins /var /var/jenkins_home && \ + chmod -R 755 /var /var/jenkins_home + # grant user jenkins access to /var/run/docker.sock RUN usermod -aG messagebus jenkins # install plugins +ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false COPY Jenkins.plugins /usr/share/jenkins/ref/plugins.txt -RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt +RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt && \ + chown -R jenkins:jenkins /var/jenkins_home USER jenkins diff --git a/Jenkins/Jenkins.plugins b/Jenkins/Jenkins.plugins index a8cb2f88..39e3d66e 100644 --- a/Jenkins/Jenkins.plugins +++ b/Jenkins/Jenkins.plugins @@ -1,8 +1,48 @@ -git -workflow-aggregator -pipeline-github-lib -docker-workflow -credentials -git-client -blueocean -coverage +authentication-tokens:latest +blueocean:latest +bouncycastle-api:latest +cloudbees-folder:latest +command-launcher:latest +configuration-as-code:latest +coverage:latest +credentials:latest +docker-build-step:latest +docker-commons:latest +docker-java-api:latest +docker-plugin:latest +docker-workflow:latest +durable-task:latest +git-client:latest +git:latest +instance-identity:latest +job-dsl:latest +junit:latest +matrix-project:latest +node-iterator-api:latest +pipeline-build-step:latest +pipeline-github-lib:latest +pipeline-groovy-lib:latest +pipeline-input-step:latest +pipeline-milestone-step:latest +pipeline-model-api:latest +pipeline-model-definition:latest +pipeline-model-extensions:latest +pipeline-rest-api:latest +pipeline-stage-step:latest +pipeline-stage-tags-metadata:latest +pipeline-stage-view:latest +pipeline-utility-steps:latest +ssh-credentials:latest +ssh-slaves:latest +workflow-aggregator:latest +workflow-basic-steps:latest +workflow-cps:latest +workflow-durable-task-step:latest +workflow-job:latest +workflow-support:latest +workflow-step-api:latest +timestamper:latest +ws-cleanup:latest +junit-attachments:latest +junit-realtime-test-reporter:latest + diff --git a/Jenkins/Jenkinsfile b/Jenkins/Jenkinsfile index 167a6ba9..9ae80ad3 100644 --- a/Jenkins/Jenkinsfile +++ b/Jenkins/Jenkinsfile @@ -1,16 +1,25 @@ +def JENKINS_UID = 1000 // UID of jenkins user from Jenkins container + pipeline { + parameters { string(name: 'AGENT_CPUS', defaultValue: '2.5', description: 'CPU limit for the build agent') string(name: 'AGENT_NETWORK', defaultValue: 'host', description: 'Network to be used for build agent') booleanParam(name: 'QUICK_RUN', defaultValue: false, description: 'false: all stages but slow, true: just some stages and fast') } + agent { dockerfile { filename 'Jenkins/jenkins-agent/Dockerfile' - args """--user root --network ${params.AGENT_NETWORK} - --volume /var/run/docker.sock:/var/run/docker.sock - --memory=8g --cpus=${params.AGENT_CPUS}""" - } + args """--user ${JENKINS_UID} --network ${params.AGENT_NETWORK} + --volume /var/run/docker.sock:/var/run/docker.sock + --memory=8g --cpus=${params.AGENT_CPUS} + --security-opt apparmor=unconfined""" + } + } + + options { + disableConcurrentBuilds() } environment { @@ -24,7 +33,7 @@ pipeline { } triggers { - pollSCM('H/1 * * * *') + pollSCM('H/2 * * * *') } stages { @@ -32,6 +41,7 @@ pipeline { steps { sh '''#!/bin/bash +x if command -v docker >/dev/null 2>&1; then + docker info --format '{{.SecurityOptions}}' if docker info --format '{{.SecurityOptions}}' 2>/dev/null | grep -q 'rootless'; then echo "🟡 Docker daemon is running in ROOTLESS mode" else diff --git a/Jenkins/Makefile b/Jenkins/Makefile index d891ac8a..bbc69eb7 100644 --- a/Jenkins/Makefile +++ b/Jenkins/Makefile @@ -1,17 +1,15 @@ include .env export -SOCKET := /var/run/docker.sock -VOLUME := jenkins_home - CERTBOT_CONF := $(PWD)/.generated/certbot/lib/conf CERTBOT_WWW := $(PWD)/.generated/certbot/lib/www CERTBOT_LOG := $(PWD)/.generated/certbot/log NGINX_LOG := $(PWD)/.generated/certbot/nginx/log -.PHONY: provision \ - build run bash init-pw unprotected protected start stop rm purge \ - nginx-prepare nginx-proxy nginx-start nginx-letsencrypt-init nginx-letsencrypt-timer nginx-restart nginx-stop +.PHONY: provision clean \ + jenkins-build jenkins-run jenkins-bash jenkins-init-pw jenkins-unprotected jenkins-protected jenkins-start jenkins-stop jenkins-rm jenkins-purge \ + nginx-prepare nginx-proxy nginx-run nginx-start nginx-letsencrypt-init nginx-letsencrypt-timer nginx-restart nginx-stop \ + jenkins-security ## lists all documented targets help: @@ -20,38 +18,51 @@ help: print " " desc "\n" \ }' $(MAKEFILE_LIST) +## uploads to hs.hsadmin.ng/Jenkins/ on the server for testing purposes +upload: + scp -r * .env .gitignore tallyman@$(SERVER_NAME):hs.hsadmin.ng/Jenkins/ + + ## initially, run this once to provision te nginx -provision: nginx-prepare nginx-letsencrypt-init nginx-letsencrypt-timer nginx-start build start +provision: nginx-prepare nginx-letsencrypt-init nginx-letsencrypt-timer jenkins-build jenkins-run nginx-restart + @echo "now you can start nginx: make nginx-start" ## removes all generated files -clean: nginx-stop stop +clean: nginx-stop jenkins-rm rm -rf .generated/ ## builds the Jenkins image -build: +jenkins-build: docker build -t jenkins-docker . -## manually running the Jenkins container -run: +# initially runs the Jenkins container during provisioning, later use `make jenkins-start` +jenkins-run: + $(eval DOCKER_SOCKET_MOUNT := $(if $(DOCKER_SOCKET),$(DOCKER_SOCKET):/var/run/docker.sock,/dev/null:/var/run/docker.no-socket)) docker run --detach \ --dns 8.8.8.8 \ --network bridge \ --publish 8090:8080 --publish 50000:50000 \ - --volume $(SOCKET):/var/run/docker.sock \ - --volume $(VOLUME):/var/jenkins_home \ + --volume $(DOCKER_SOCKET_MOUNT) \ + --volume $(JENKINS_VOLUME):/var/jenkins_home \ + --volume $(PWD)/jenkins.yaml:/var/jenkins_home/jenkins.yaml \ --restart unless-stopped \ + --env-file .env \ --name jenkins jenkins-docker ## manually starts the Jenkins container (again) -start: +jenkins-start: docker start jenkins ## opens a bash within the Jenkins container -bash: +jenkins-bash: docker exec -it jenkins bash +## prints the Jenkins log +jenkins-log: + docker logs jenkins 2>&1 + ## prints the initial password of a newly setup Jenkins -init-pw: +jenkins-init-pw: docker exec -it jenkins sh -c '\ while [ ! -f /var/jenkins_home/secrets/initialAdminPassword ]; do \ sleep 1; \ @@ -60,50 +71,44 @@ init-pw: ' ## disables security for the Jenkins => allows login to Jenkins without credentials -unprotected: +jenkins-unprotected: docker exec -it jenkins sed -i 's|true|false|' /var/jenkins_home/config.xml docker exec -it jenkins grep useSecurity /var/jenkins_home/config.xml ## enables security for the Jenkins => Jenkins requires login with credentials -protected: +jenkins-protected: docker exec -it jenkins sed -i 's|true|true|' /var/jenkins_home/config.xml docker exec -it jenkins grep useSecurity /var/jenkins_home/config.xml ## stops the Jenkins container -stop: - docker stop jenkins +jenkins-stop: + docker stop jenkins || true ## removes the Jenkins container -rm: stop - docker rm jenkins +jenkins-rm: jenkins-stop + docker rm jenkins || true ## purges the Jenkins volume (finally deletes the configuration) -purge: rm - docker volume rm $(VOLUME) +jenkins-purge: jenkins-rm + docker volume rm $(JENKINS_VOLUME) || true # (internal) generates the files for nginx-proxy and certbot nginx-prepare: mkdir -p $(CERTBOT_WWW) $(CERTBOT_LOG) $(CERTBOT_CONF)/live/$(SERVER_NAME) $(NGINX_LOG) chmod 755 $(CERTBOT_WWW) $(CERTBOT_LOG) $(CERTBOT_CONF)/live/$(SERVER_NAME) $(NGINX_LOG) - sed -e 's/%SERVER_NAME/$(SERVER_NAME)/g' .generated/nginx.conf + sed -e 's/%SERVER_NAME/$(SERVER_NAME)/g' .generated/nginx.conf cp nginx-proxy/options-ssl-nginx.conf $(CERTBOT_CONF)/options-ssl-nginx.conf chmod 644 $(CERTBOT_CONF)/options-ssl-nginx.conf test -f $(CERTBOT_CONF)/ssl-dhparams.pem || curl -o $(CERTBOT_CONF)/ssl-dhparams.pem \ https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem chmod 644 $(CERTBOT_CONF)/ssl-dhparams.pem - openssl req -x509 -nodes -newkey rsa:2048 \ - -keyout $(CERTBOT_CONF)/live/$(SERVER_NAME)/privkey.pem \ - -out /$(CERTBOT_CONF)/live/$(SERVER_NAME)/fullchain.pem \ - -subj "/CN=dummy" ## opens a bash within the Nginx-proxy container nginx-bash: docker exec -it nginx bash # (internal) fetches an initial certificate from letsencrypt -nginx-letsencrypt-init: nginx-start - # wait for nginx actually running (could be improved) - @sleep 5 +nginx-letsencrypt-init: nginx-run # delete the previous (dummy) config to avoid file creation with suffix -0001 etc. rm -rf $(CERTBOT_CONF)/etc/letsencrypt/live/$(SERVER_NAME) \ $(CERTBOT_CONF)/etc/letsencrypt/archive/$(SERVER_NAME) \ @@ -114,13 +119,12 @@ nginx-letsencrypt-init: nginx-start -v $(CERTBOT_WWW):/var/www/certbot \ -v $(CERTBOT_LOG):/var/log/letsencrypt \ certbot/certbot \ - certonly --webroot --webroot-path /var/www/certbot \ - --email $(EMAIL) --cert-name $(SERVER_NAME) \ + certonly --webroot --webroot-path /var/www/certbot --cert-name $(SERVER_NAME) \ -d $(SERVER_NAME) --rsa-key-size 4096 \ - --agree-tos --force-renewal - # restart nginx + --non-interactive --agree-tos --force-renewal $(CERTBOT_ENV) + # from now on, start nginx including https + sed -e 's/%SERVER_NAME/$(SERVER_NAME)/g' .generated/nginx.conf docker stop nginx || true - docker start nginx ## opens a shell in the letsencrypt certbot nginx-letsencrypt-sh: @@ -147,8 +151,8 @@ nginx-letsencrypt-renew: -v $(CERTBOT_LOG):/var/log/letsencrypt \ certbot/certbot renew -q -## starts the nginx proxy server -nginx-start: nginx-stop +## initially runs the nginx proxy server +nginx-run: nginx-stop docker run -d --name nginx \ --publish 8080:80 \ --publish 8443:443 \ @@ -157,8 +161,16 @@ nginx-start: nginx-stop -v $(CERTBOT_WWW):/var/www/certbot \ -v $(NGINX_LOG):/var/log/nginx \ -v $(PWD)/.generated/nginx.conf:/etc/nginx/nginx.conf \ + --health-cmd="curl -kfs https://localhost:8443/ || exit 1" \ + --health-interval=5s \ + --health-timeout=3s \ + --health-retries=3 \ nginx +## starts the nginx proxy server again +nginx-start: + docker start nginx + ## restarts the nginx proxy server nginx-restart: nginx-stop nginx-start @@ -167,3 +179,15 @@ nginx-stop: docker stop nginx || true docker rm nginx || true +## remove the nginx container +nginx-rm: nginx-stop + docker rm nginx || true + +## check security status +jenkins-security: + @curl --insecure -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/script + +## fix access rights in workspaces +jenkins-fix: + @docker run --rm -it -v $(JENKINS_VOLUME):/mnt alpine chown 1000:1000 -R /mnt/workspace + diff --git a/Jenkins/README.md b/Jenkins/README.md index 9b5422c9..d7d52cf4 100644 --- a/Jenkins/README.md +++ b/Jenkins/README.md @@ -5,12 +5,24 @@ The scripts work in a Hostsharing Managed Docker environment. Requires a .env file like this in the current directory: ``` +DOCKER_SOCKET=/var/run/docker.sock +DOCKER_HOST=unix:///var/run/docker.soc SERVER_NAME=jenkins.example.org -EMAIL=contact@example.org +JENKINS_VOLUME=jenkins_home +JENKINS_ADMIN_PASSWORD=password-for-initial-user-admin +GIT_USERNAME=git-username +GIT_PASSWORD=git-password +CERTBOT_ENV=--staging # leave empty for real certificates or --staging for test certificates ``` Then run `make provision` to initialize everything. -Run `make help` for more information. +To completely start over again, run `make jenkins-purge clean provision`. +This will also remove all Jenkins configurations! -WARNING: Provisioning does not really work yet, needs some manual restarts. +Once everything works, you can remove `--staging` from `.env` +and run `make clean provision`. +Now, a *letsencrypt* is asked to issue a real certificate. +Beware, this is only possible 5 times per 24h. + +Run `make help` for more information. diff --git a/Jenkins/jenkins-agent/Dockerfile b/Jenkins/jenkins-agent/Dockerfile index e0d571f8..b9238f26 100644 --- a/Jenkins/jenkins-agent/Dockerfile +++ b/Jenkins/jenkins-agent/Dockerfile @@ -1,4 +1,10 @@ FROM eclipse-temurin:21-jdk + +# create mount point for jenkins_home +RUN mkdir -p /var/jenkins_home && \ + chmod 755 /var/jenkins_home + +# install required packages RUN apt-get update && \ apt-get install -y \ postgresql-client \ @@ -7,3 +13,6 @@ RUN apt-get update && \ pandoc && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* + +# continue with the same uid as the user 'jenkins' from the Jenkins Dockerfile +USER 1000 diff --git a/Jenkins/jenkins.yaml b/Jenkins/jenkins.yaml new file mode 100644 index 00000000..e110d677 --- /dev/null +++ b/Jenkins/jenkins.yaml @@ -0,0 +1,118 @@ +jenkins: + systemMessage: "Jenkins configuration via Jenkins Configuration as Code" + authorizationStrategy: + loggedInUsersCanDoAnything: + allowAnonymousRead: true + clouds: + - docker: + name: "docker" + dockerApi: + dockerHost: + uri: "${DOCKER_HOST}" + connectTimeout: 60 + readTimeout: 60 + containerCap: 10 + remotingSecurity: + enabled: true + securityRealm: + local: + allowsSignup: false + enableCaptcha: false + users: + - id: "admin" + name: "admin" + password: "${JENKINS_ADMIN_PASSWORD}" + properties: + - "consoleUrlProvider" + - "favorite" + - "myView" + - preferredProvider: + providerId: "default" + - theme: + theme: "noOp" + - "timezone" + - "experimentalFlags" + - mailer: + emailAddress: "michael.hoennig@hostsharing.net" + - "apiToken" + +credentials: + system: + domainCredentials: + - credentials: + # Username/password credential + - usernamePassword: + scope: GLOBAL + id: 'hsadmin-NG-git' + username: "${GIT_USERNAME}" + password: "${GIT_PASSWORD}" + description: 'git access' + +jobs: + - script: > + multibranchPipelineJob('hsadmin-NG Java backend') { + branchSources { + git { + id('hsadmin-NG') + remote('https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng') + credentialsId('hsadmin-NG-git') + } + } + factory { + workflowBranchProjectFactory { + scriptPath('Jenkins/Jenkinsfile') + } + } + triggers { + periodicFolderTrigger { + interval('2m') + } + } + } + +security: + apiToken: + creationOfLegacyTokenEnabled: false + tokenGenerationOnCreationEnabled: false + usageStatisticsEnabled: true + cps: + hideSandbox: false + gitHooks: + allowedOnAgents: false + allowedOnController: false + gitHostKeyVerificationConfiguration: + sshHostKeyVerificationStrategy: "knownHostsFileVerificationStrategy" + globalJobDslSecurityConfiguration: + useScriptSecurity: true + scriptApproval: + forceSandbox: true + location: + adminAddress: "michael.hoennig@hostsharing.net" + url: "https://vm2176.hostsharing.net/" + mailer: + charset: "UTF-8" + useSsl: false + useTls: false + pollSCM: + pollingThreadCount: 10 + scmGit: + addGitTagAction: false + allowSecondFetch: false + createAccountBasedOnEmail: false + disableGitToolChooser: false + hideCredentials: false + showEntireCommitSummaryInChanges: false + useExistingAccountWithSameEmail: false + timestamper: + allPipelines: false + elapsedTimeFormat: "''HH:mm:ss.S' '" + systemTimeFormat: "''HH:mm:ss' '" + +tool: + git: + installations: + - home: "git" + name: "Default" + mavenGlobalConfig: + globalSettingsProvider: "standard" + settingsProvider: "standard" diff --git a/Jenkins/nginx-proxy/nginx-init.conf b/Jenkins/nginx-proxy/nginx-init.conf new file mode 100644 index 00000000..4dcbeed7 --- /dev/null +++ b/Jenkins/nginx-proxy/nginx-init.conf @@ -0,0 +1,19 @@ +events {} + +http { + server { + listen 80; + server_name %SERVER_NAME; + + # directly answer initial certbot request + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # forward all other HTTP-requests to HTTPS + location / { + return 301 https://$host$request_uri; + } + } +} +