Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor fixes for least privilege environments #209

Merged
merged 6 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ COPY src/main /app/src/main
COPY compiler.groovy /app

WORKDIR /app
# Exclude code not needed in productive image
RUN cd /app/src/main/groovy/com/cloudogu/gitops/cli/ \
&& rm GenerateJsonSchema.groovy \
&& rm GitopsPlaygroundCliMainScripted.groovy
# Build native image without micronaut
RUN ./mvnw package -DskipTests
# Use simple name for largest jar file -> Easier reuse in later stages
Expand All @@ -50,7 +54,7 @@ RUN apk add --no-cache \
gnupg \
outils-sha256 \
git \
bash curl unzip
bash curl unzip zip

RUN mkdir -p /dist/usr/local/bin
RUN mkdir -p /dist/home/.config
Expand All @@ -67,7 +71,7 @@ RUN tar -xf helm.tar.gz
RUN echo "${HELM_CHECKSUM} helm.tar.gz" | sha256sum -c
RUN set -o pipefail && curl --location --fail --retry 20 --retry-connrefused --retry-all-errors \
https://raw.githubusercontent.com/helm/helm/main/KEYS | gpg --import --batch --no-default-keyring --keyring /tmp/keyring.gpg
RUN gpgv --keyring /tmp/keyring.gpg helm.tar.gz.asc helm.tar.gz
RUN gpgv --keyring /tmp/keyring.gpg helm.tar.gz.asc helm.tar.gz
RUN mv linux-amd64/helm /dist/usr/local/bin
ENV PATH=$PATH:/dist/usr/local/bin

Expand Down Expand Up @@ -107,9 +111,19 @@ RUN rm -r /dist/app/.mvn
RUN rm /dist/app/mvnw
RUN rm /dist/app/pom.xml
RUN rm /dist/app/compiler.groovy
RUN rm -r /dist/app/src/test
RUN cd /dist/app/scripts && rm downloadHelmCharts.sh apply-ng.sh
# For dev image
RUN mv /dist/app/src /src-without-graal && rm -r /src-without-graal/main/groovy/com/cloudogu/gitops/graal
RUN mkdir /dist-dev
# Remove uncessary code and allow changing code in dev mode, less secure, but the intention of the dev image
# Execute bit is required to allow listing of dirs to everyone
RUN mv /dist/app/src /dist-dev/src && \
chmod a=rwx -R /dist-dev/src && \
rm -r /dist-dev/src/main/groovy/com/cloudogu/gitops/graal
# Remove compiled GOP code from jar to avoid duplicate in dev image, allowing for scripting
COPY --from=maven-build /app/gitops-playground.jar /dist-dev/gitops-playground.jar
RUN zip -d /dist-dev/gitops-playground.jar 'com/cloudogu/gitops/*'

# Required to prevent Java exceptions resulting from AccessDeniedException by jgit when running arbitrary user
RUN mkdir -p /dist/root/.config/jgit
RUN touch /dist/root/.config/jgit/config
Expand All @@ -136,8 +150,13 @@ RUN tar -xvzf x86_64-linux-musl-native.tgz -C ${RESULT_LIB} --strip-components 1
ENV CC=/musl/bin/gcc
RUN curl --location --fail --retry 20 --retry-connrefused -o zlib.tar.gz https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/zlib-${ZLIB_VERSION}.tar.gz
RUN curl --location --fail --retry 20 --retry-connrefused -o zlib.tar.gz.asc https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/zlib-${ZLIB_VERSION}.tar.gz.asc
RUN gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys 5ED46A6721D365587791E2AA783FCD8E58BCAFBA # madler@alumni.caltech.edu
RUN gpg --batch --verify zlib.tar.gz.asc zlib.tar.gz
# Use curl instead of "gpg --recv-keys" for more stable builds with retries
# Key of the zlib maintainer: madler@alumni.caltech.edu
ENV ZLIB_KEY=5ED46A6721D365587791E2AA783FCD8E58BCAFBA
RUN set -o pipefail && curl --silent --show-error --location --fail --retry 20 --retry-connrefused --retry-delay 5 \
"https://keys.openpgp.org/vks/v1/by-fingerprint/${ZLIB_KEY}" | \
gpg --import --batch --no-default-keyring --keyring /tmp/keyring.gpg
RUN gpgv --keyring /tmp/keyring.gpg zlib.tar.gz.asc zlib.tar.gz
RUN mkdir zlib && tar -xvzf zlib.tar.gz -C zlib --strip-components 1 && \
cd zlib && ./configure --static --prefix=/musl && \
make && make install && \
Expand Down Expand Up @@ -196,9 +215,10 @@ ENTRYPOINT ["/app/apply-ng"]
FROM eclipse-temurin:${JDK_VERSION}-jre-alpine as dev

# apply-ng.sh is part of the dev image and allows trying changing groovy code inside the image for debugging
COPY scripts/apply-ng.sh /app/scripts/
COPY --from=maven-build /app/gitops-playground.jar /app/
COPY --from=downloader /src-without-graal /app/src
# Allow changing code in dev mode, less secure, but the intention of the dev image
COPY --chmod=777 scripts/apply-ng.sh /app/scripts/
COPY --from=downloader /dist-dev /app

# Allow initialization in final FROM ${ENV} stage
USER 0
# Avoids ERROR org.eclipse.jgit.util.FS - Cannot save config file 'FileBasedConfig[/app/?/.config/jgit/config]'
Expand All @@ -211,7 +231,7 @@ ENTRYPOINT [ "java", \
"org.codehaus.groovy.tools.GroovyStarter", \
"--main", "groovy.ui.GroovyMain", \
"--classpath", "/app/src/main/groovy", \
"/app/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMain.groovy" ]
"/app/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy" ]

# Pick final image according to build-arg
FROM ${ENV}
Expand Down
10 changes: 5 additions & 5 deletions docs/developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,14 +746,14 @@ docker run --rm --entrypoint java gitops-playground:dev -classpath /app/gitops-p
On `main` branch:

````shell
TAG=0.2.0
TAG=0.5.0

git checkout main
git pull
git tag -s $TAG -m $TAG
git push --follow-tags
[[ $? -eq 0 ]] && git pull
[[ $? -eq 0 ]] && git tag -s $TAG -m $TAG
[[ $? -eq 0 ]] && git push --follow-tags

xdg-open https://ecosystem.cloudogu.com/jenkins/job/cloudogu-github/job/gitops-playground/job/main/build?delay=0sec
[[ $? -eq 0 ]] && xdg-open https://ecosystem.cloudogu.com/jenkins/job/cloudogu-github/job/gitops-playground/job/main/build?delay=0sec
````

For now, please start a Jenkins Build of `main` manually.
Expand Down
9 changes: 7 additions & 2 deletions scripts/apply-ng.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail

TRACE=${TRACE:-}
[[ $TRACE == true ]] && set -x;

ABSOLUTE_BASEDIR="$(cd "$(dirname $0)" && pwd)"
PLAYGROUND_DIR="$(cd ${ABSOLUTE_BASEDIR} && cd .. && pwd)"
# Allow for overriding the folder to jar via env var
export CLASSPATH="${CLASSPATH:-${PLAYGROUND_DIR}/gitops-playground.jar}"

function apply-ng() {
groovy --classpath "$PLAYGROUND_DIR"/src/main/groovy \
"$PLAYGROUND_DIR"/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMain.groovy "$@"
"$PLAYGROUND_DIR"/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy "$@"
}

# Runs groovy files without needing groovy
function groovy() {
# We don't need the groovy "binary" (script) to start, because the gitops-playground.jar already contains groovy-all.
# Note that gitops-playground.jar is passed via env var CLASSPATH

# Set params like startGroovy does (which is called by the "groovy" script)
# See https://github.com/apache/groovy/blob/master/src/bin/startGroovy
java \
-classpath "$PLAYGROUND_DIR"/gitops-playground.jar \
org.codehaus.groovy.tools.GroovyStarter \
--main groovy.ui.GroovyMain \
"$@"
Expand Down
2 changes: 1 addition & 1 deletion src/main/groovy/com/cloudogu/gitops/Application.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import jakarta.inject.Singleton
@Singleton
class Application {

private final List<Feature> features
final List<Feature> features

Application(
List<Feature> features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ class GitopsPlaygroundCli implements Runnable {
}

def config = getConfig(context, false)
context = context.registerSingleton(new Configuration(config))
register(context, new Configuration(config))

K8sClient k8sClient = context.getBean(K8sClient)

if (config['application']['destroy']) {
Expand All @@ -247,6 +248,10 @@ class GitopsPlaygroundCli implements Runnable {
}
}

protected void register(ApplicationContext context, Configuration configuration) {
context.registerSingleton(configuration)
}

private void confirmOrExit(String message, Map config) {
if (config['application']['yes']) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import picocli.CommandLine
@Slf4j
class GitopsPlaygroundCliMain {

Class<?> commandClass = GitopsPlaygroundCli.class

static void main(String[] args) throws Exception {
new GitopsPlaygroundCliMain().exec(args)
new GitopsPlaygroundCliMain().exec(args, GitopsPlaygroundCli.class)
}

@SuppressWarnings('GrMethodMayBeStatic') // Non-static for easier testing
void exec(String[] args) {
@SuppressWarnings('GrMethodMayBeStatic') // Non-static for easier testing and reuse
void exec(String[] args, Class<?> commandClass) {
// log levels can be set via picocli.trace sys env - defaults to 'WARN'
if (args.contains('--trace') || args.contains('-x'))
System.setProperty("picocli.trace", "DEBUG")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.cloudogu.gitops.cli

import com.cloudogu.gitops.Application
import com.cloudogu.gitops.config.ApplicationConfigurator
import com.cloudogu.gitops.config.ConfigToConfigFileConverter
import com.cloudogu.gitops.config.Configuration
import com.cloudogu.gitops.config.schema.JsonSchemaGenerator
import com.cloudogu.gitops.config.schema.JsonSchemaValidator
import com.cloudogu.gitops.dependencyinjection.HttpClientFactory
import com.cloudogu.gitops.dependencyinjection.JenkinsFactory
import com.cloudogu.gitops.dependencyinjection.RetrofitFactory
import com.cloudogu.gitops.destroy.ArgoCDDestructionHandler
import com.cloudogu.gitops.destroy.Destroyer
import com.cloudogu.gitops.destroy.JenkinsDestructionHandler
import com.cloudogu.gitops.destroy.ScmmDestructionHandler
import com.cloudogu.gitops.features.*
import com.cloudogu.gitops.features.argocd.ArgoCD
import com.cloudogu.gitops.features.deployment.ArgoCdApplicationStrategy
import com.cloudogu.gitops.features.deployment.Deployer
import com.cloudogu.gitops.features.deployment.HelmStrategy
import com.cloudogu.gitops.jenkins.*
import com.cloudogu.gitops.scmm.ScmmRepoProvider
import com.cloudogu.gitops.utils.*
import groovy.util.logging.Slf4j
import io.micronaut.context.ApplicationContext
import jakarta.inject.Provider
/**
* Micronaut's dependency injection relies on statically compiled class files with seems incompatible with groovy
* scripting/interpretation (without prior compilation).
* The purpose of our -dev image is exactly that: allow groovy scripting inside the image, to shorten the dev cycle on
* air-gapped customer envs.
*
* To make this work the dev image gets it's own main() method that explicitly creates instances of the groovy classes.
* Yes, redundant and not beautiful, but not using dependency injection is worse.
*/
@Slf4j
class GitopsPlaygroundCliMainScripted {

static void main(String[] args) throws Exception {
new GitopsPlaygroundCliMain().exec(args, GitopsPlaygroundCliScripted.class)
}

static class GitopsPlaygroundCliScripted extends GitopsPlaygroundCli {

@Override
protected ApplicationContext createApplicationContext() {
ApplicationContext context = super.createApplicationContext()

// Create ApplicationConfigurator to get started
context.registerSingleton(
new ApplicationConfigurator(
new NetworkingUtils(new K8sClient(new CommandExecutor(), new FileSystemUtils(), null), new CommandExecutor()),
new FileSystemUtils(),
new JsonSchemaValidator(new JsonSchemaGenerator())))
context.registerSingleton(new ConfigToConfigFileConverter())
return context
}

@Override
protected void register(ApplicationContext context, Configuration config) {
super.register(context, config)

// After config is set, create all other beans

def fileSystemUtils = new FileSystemUtils()
def executor = new CommandExecutor()
def k8sClient = new K8sClient(executor, fileSystemUtils, new Provider<Configuration>() {
@Override
Configuration get() {
return config
}
})
def helmClient = new HelmClient(executor)

def httpClientFactory = new HttpClientFactory()

def scmmRepoProvider = new ScmmRepoProvider(config, fileSystemUtils)
def retrofitFactory = new RetrofitFactory()

def insecureSslContextProvider = new Provider<HttpClientFactory.InsecureSslContext>() {
@Override
HttpClientFactory.InsecureSslContext get() {
return httpClientFactory.insecureSslContext()
}
}
def httpClientScmm = retrofitFactory.okHttpClient(httpClientFactory.createLoggingInterceptor(), config, insecureSslContextProvider)
def retrofit = retrofitFactory.retrofit(config, httpClientScmm)
def repoApi = retrofitFactory.repositoryApi(retrofit)

def jenkinsConfiguration = new JenkinsConfigurationAdapter(config)
JenkinsFactory jenkinsFactory = new JenkinsFactory(jenkinsConfiguration)
def jenkinsApiClient = jenkinsFactory.jenkinsApiClient(
httpClientFactory.okHttpClient(httpClientFactory.createLoggingInterceptor(), jenkinsConfiguration, insecureSslContextProvider))

context.registerSingleton(k8sClient)

if (config.config['application']['destroy']) {
context.registerSingleton(new Destroyer([
new ArgoCDDestructionHandler(config, k8sClient, scmmRepoProvider, helmClient, fileSystemUtils),
new ScmmDestructionHandler(config, retrofitFactory.usersApi(retrofit), retrofitFactory.repositoryApi(retrofit)),
new JenkinsDestructionHandler(new JobManager(jenkinsApiClient), config, new GlobalPropertyManager(jenkinsApiClient))
]))
} else {
def helmStrategy = new HelmStrategy(config, helmClient)

def deployer = new Deployer(config, new ArgoCdApplicationStrategy(config, fileSystemUtils, scmmRepoProvider), helmStrategy)

def airGappedUtils = new AirGappedUtils(config, scmmRepoProvider, repoApi, fileSystemUtils, helmClient)

context.registerSingleton(new Application([
new Registry(config, fileSystemUtils, k8sClient, helmStrategy),
new ScmManager(config, executor, fileSystemUtils, helmStrategy),
new Jenkins(config, executor, fileSystemUtils, new GlobalPropertyManager(jenkinsApiClient),
new JobManager(jenkinsApiClient), new UserManager(jenkinsApiClient),
new PrometheusConfigurator(jenkinsApiClient)),
new ArgoCD(config, k8sClient, helmClient, fileSystemUtils, scmmRepoProvider),
new IngressNginx(config, fileSystemUtils, deployer, k8sClient, airGappedUtils),
new Mailhog(config, fileSystemUtils, deployer, k8sClient, airGappedUtils),
new PrometheusStack(config, fileSystemUtils, deployer, k8sClient, airGappedUtils),
new ExternalSecretsOperator(config, fileSystemUtils, deployer, k8sClient, airGappedUtils),
new Vault(config, fileSystemUtils, k8sClient, deployer, airGappedUtils)
]))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ class ApplicationConfigurator {
password : DEFAULT_ADMIN_PW,
yes : false,
runningInsideK8s : false, // Set dynamically
clusterBindAddress : '', // Set dynamically
namePrefix : '',
podResources : false,
namePrefixForEnvVars : '', // Set dynamically
Expand Down Expand Up @@ -348,9 +347,6 @@ class ApplicationConfigurator {
log.debug("installation is running in kubernetes.")
newConfig.application["runningInsideK8s"] = true
}
String clusterBindAddress = networkingUtils.findClusterBindAddress()
log.debug("Setting cluster bind Address: " + clusterBindAddress)
newConfig.application["clusterBindAddress"] = clusterBindAddress
}

private void addScmmConfig(Map newConfig) {
Expand All @@ -368,8 +364,8 @@ class ApplicationConfigurator {
} else {
log.debug("Setting internal configs for local single node cluster with internal scmm")
def port = fileSystemUtils.getLineFromFile(fileSystemUtils.getRootDir() + "/scm-manager/values.ftl.yaml", "nodePort:").findAll(/\d+/)*.toString().get(0)
String cba = newConfig.application["clusterBindAddress"]
newConfig.scmm["url"] = networkingUtils.createUrl(cba, port, "/scm")
String clusterBindAddress = networkingUtils.findClusterBindAddress()
newConfig.scmm["url"] = networkingUtils.createUrl(clusterBindAddress, port, "/scm")
}

String scmmUrl = newConfig.scmm["url"]
Expand All @@ -396,8 +392,8 @@ class ApplicationConfigurator {
} else {
log.debug("Setting jenkins configs for local single node cluster with internal jenkins")
def port = fileSystemUtils.getLineFromFile(fileSystemUtils.getRootDir() + "/jenkins/values.yaml", "nodePort:").findAll(/\d+/)*.toString().get(0)
String cba = newConfig.application["clusterBindAddress"]
newConfig.jenkins["url"] = networkingUtils.createUrl(cba, port)
String clusterBindAddress = networkingUtils.findClusterBindAddress()
newConfig.jenkins["url"] = networkingUtils.createUrl(clusterBindAddress, port)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ class Schema {
@JsonPropertyDescription(PIPE_YES_DESCRIPTION)
boolean yes = false
// boolean runningInsideK8s = ""
// String clusterBindAddress = ""
@JsonPropertyDescription(NAME_PREFIX_DESCRIPTION)
String namePrefix = ""

Expand Down
5 changes: 2 additions & 3 deletions src/main/groovy/com/cloudogu/gitops/destroy/Destroyer.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import jakarta.inject.Singleton

@Singleton
@Slf4j
class Destroyer implements DestructionHandler {
class Destroyer {

private final List<DestructionHandler> destructionHandlers
final List<DestructionHandler> destructionHandlers

Destroyer(List<DestructionHandler> destructionHandlers) {
this.destructionHandlers = destructionHandlers
}

@Override
void destroy() {
log.info("Start destroying")
for (def handler in destructionHandlers) {
Expand Down
6 changes: 2 additions & 4 deletions src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,8 @@ class Jenkins extends Feature {
"${config.registry['password']}",
'credentials for accessing the docker-registry')
}
// Once everything is set up, start the jobs.
jobManger.startJob('example-apps')
}

// Once everything is set up, start the jobs.
jobManger.startJob('example-apps')

}
}
Loading