diff --git a/.dockerignore b/.dockerignore index 6df0bc1..7b6d2b2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,2 @@ Dockerfile forgejo-runner -/examples/docker-compose/srv/ \ No newline at end of file diff --git a/.forgejo/workflows/example-docker-compose.yml b/.forgejo/workflows/example-docker-compose.yml index aeff9fc..6e017db 100644 --- a/.forgejo/workflows/example-docker-compose.yml +++ b/.forgejo/workflows/example-docker-compose.yml @@ -28,8 +28,6 @@ jobs: - name: run the example run: | set -x - mkdir -p /srv/runner-data - chown 1000:1000 /srv/runner-data cd examples/docker-compose secret=$(openssl rand -hex 20) sed -i -e "s/{SHARED_SECRET}/$secret/" compose-forgejo-and-runner.yml @@ -37,7 +35,7 @@ jobs: # # Launch Forgejo & the runner # - $cli up -d --remove-orphans + $cli up -d for delay in $(seq 60) ; do test -f /srv/runner-data/.runner && break ; sleep 30 ; done test -f /srv/runner-data/.runner # diff --git a/Dockerfile b/Dockerfile index c43c34e..6acc805 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,10 +40,8 @@ ENV HOME=/data USER 1000:1000 -COPY --chmod=555 entrypoint.sh /entrypoint.sh - WORKDIR /data VOLUME ["/data"] -ENTRYPOINT ["/entrypoint.sh"] +CMD ["/bin/forgejo-runner"] diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index e7f7ae4..b28f9ad 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## 6.0.0 + +* Security: the container options a job is allowed to specify are limited to a [predefined allow list](https://forgejo.org/docs/next/user/actions/#jobsjob_idcontaineroptions). + ## 5.0.4 * Define FORGEJO_TOKEN as an alias to GITHUB_TOKEN diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 7eea913..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Technically not necessary, but it cleans up the logs from having token/secret values -run_command() { - local cmd="$@" - # Replace any --token or --secret with [REDACTED] - local safe_cmd=$(echo "$cmd" | sed -E 's/--(token|secret) [^ ]+/--\1 [REDACTED]/g') - decho "Running command: $safe_cmd" - eval $cmd -} - -decho() { - if [[ "${DEBUG}" == "true" ]]; then - echo "[entrypoint] $@" - fi -} -decho $PWD - -# Check if the script is running as root -if [[ $(id -u) -eq 0 ]]; then - ISROOT=true - decho "[WARNING] Running as root user" -fi - -# Handle if `command` is passed, as command appends arguments to the entrypoint -if [ "$#" -gt 0 ]; then - run_command $@ - exit -fi - -# Set default values (if needed) -RUNNER__runner__FILE="${RUNNER__runner__FILE:-/data/runner.json}" -RUNNER__CONFIG_FILE="${RUNNER__CONFIG_FILE:-/data/runner.yml}" - -ENV_FILE="${ENV_FILE:-/data/.env}" -# Set config arguments -CONFIG_ARG="--config ${RUNNER__CONFIG_FILE}" -# Show config variables -decho "CONFIG: ${CONFIG_ARG}" - -# Generate config if not found -if [[ ! -f "${RUNNER__CONFIG_FILE}" ]]; then - echo "Creating ${RUNNER__CONFIG_FILE}" - run_command "forgejo-runner generate-config > ${RUNNER__CONFIG_FILE}" -fi - -# Use environment variables directly in the config, no need for sed edits -decho "Using config from: ${RUNNER__CONFIG_FILE}" -decho "Using environment file: ${ENV_FILE}" - -# Set extra arguments from environment variables -EXTRA_ARGS="" -if [[ -n "${RUNNER__container__LABELS}" ]]; then - EXTRA_ARGS="${EXTRA_ARGS} --labels ${RUNNER__container__LABELS}" -fi -decho "EXTRA_ARGS: ${EXTRA_ARGS}" - -if [[ "${SKIP_WAIT}" != "true" ]]; then - echo "Waiting 10s to allow other services to start up..." - sleep 10 -fi - -FORGEJO_URL="${FORGEJO_URL:-http://forgejo:3000}" - -# Try to register the runner -if [[ ! -s "${RUNNER__runner__FILE}" ]]; then - touch ${RUNNER__runner__FILE} - try=$((try + 1)) - success=0 - decho "try: ${try}, success: ${success}" - - while [[ $success -eq 0 ]] && [[ $try -lt ${MAX_REG_ATTEMPTS:-10} ]]; do - if [[ -n "${FORGEJO_SECRET}" ]]; then - run_command forgejo-runner create-runner-file --connect \ - --instance "${FORGEJO_URL}" \ - --name "${RUNNER__NAME:-$(hostname)}" \ - --secret "${FORGEJO_SECRET}" \ - ${CONFIG_ARG} \ - ${EXTRA_ARGS} 2>&1 | tee /tmp/reg.log - else - run_command forgejo-runner register \ - --instance "${FORGEJO_URL}" \ - --name "${RUNNER__NAME:-$(hostname)}" \ - --token "${RUNNER_TOKEN}" \ - --no-interactive \ - ${CONFIG_ARG} \ - ${EXTRA_ARGS} 2>&1 | tee /tmp/reg.log - fi - cat /tmp/reg.log | grep -E 'connection successful|registered successfully' >/dev/null - if [[ $? -eq 0 ]]; then - echo "SUCCESS" - success=1 - else - echo "Waiting to retry ..." - sleep 5 - fi - decho "try: ${try}, success: ${success}" - done -fi - -# Prevent reading the token from the forgejo-runner process -unset RUNNER_TOKEN -unset FORGEJO_SECRET - -run_command forgejo-runner daemon ${CONFIG_ARG} diff --git a/examples/docker-compose/.gitignore b/examples/docker-compose/.gitignore deleted file mode 100644 index 94bf3ec..0000000 --- a/examples/docker-compose/.gitignore +++ /dev/null @@ -1 +0,0 @@ -srv diff --git a/examples/docker-compose/compose-demo-workflow.yml b/examples/docker-compose/compose-demo-workflow.yml index 844dbdc..90e7d52 100644 --- a/examples/docker-compose/compose-demo-workflow.yml +++ b/examples/docker-compose/compose-demo-workflow.yml @@ -7,8 +7,6 @@ services: image: code.forgejo.org/oci/alpine:3.19 links: - forgejo - depends_on: - - runner-daemon command: >- sh -ec ' apk add --quiet git curl jq ; @@ -16,14 +14,14 @@ services: cd /srv/demo ; git init --initial-branch=main ; mkdir -p .forgejo/workflows ; - echo "{ name: \"Demo All Good\", on: [push], jobs: { test: { runs-on: docker, steps: [ {uses: actions/checkout@v4}, { run: echo All Good } ] } } }" > .forgejo/workflows/demo.yml ; - echo "{ name: \"Demo Docker\", on: [push], jobs: { test_docker: { runs-on: ubuntu-22.04, steps: [ { run: docker info } ] } } }" > .forgejo/workflows/demo_docker.yml ; + echo "{ on: [push], jobs: { test: { runs-on: docker, steps: [ {uses: actions/checkout@v4}, { run: echo All Good } ] } } }" > .forgejo/workflows/demo.yml ; + echo "{ on: [push], jobs: { test_docker: { runs-on: ubuntu-22.04, steps: [ { run: docker info } ] } } }" > .forgejo/workflows/demo_docker.yml ; git add . ; git config user.email root@example.com ; git config user.name username ; git commit -m demo ; while : ; do - git push --set-upstream --force http://root:ROOT_PASSWORD@forgejo:3000/root/test main && break ; + git push --set-upstream --force http://root:{ROOT_PASSWORD}@forgejo:3000/root/test main && break ; sleep 5 ; done ; sha=`git rev-parse HEAD` ; diff --git a/examples/docker-compose/compose-forgejo-and-runner.yml b/examples/docker-compose/compose-forgejo-and-runner.yml index 3c3ae0d..4794985 100644 --- a/examples/docker-compose/compose-forgejo-and-runner.yml +++ b/examples/docker-compose/compose-forgejo-and-runner.yml @@ -11,83 +11,83 @@ # NOTE: a token obtained from the Forgejo web interface cannot be used # as a shared secret. # -# Replace ${RUNNER_TOKEN} with the token obtained from the Forgejo web interface. -# -# Replace ROOT_PASSWORD with a secure password. +# Replace {ROOT_PASSWORD} with a secure password # + volumes: docker_certs: services: + docker-in-docker: image: code.forgejo.org/oci/docker:dind - hostname: docker # Must set hostname for both internal DNS and TLS to work as certs are only valid for docker and localhost - restart: unless-stopped + hostname: docker # Must set hostname as TLS certificates are only valid for docker or localhost privileged: true environment: - DOCKER_TLS_CERTDIR: "/certs" # set to "" to disable the use of TLS, also manually update existing runner configs to use port 2375 - DOCKER_HOST: "docker" # remove aswell to disable TLS + DOCKER_TLS_CERTDIR: /certs + DOCKER_HOST: docker-in-docker volumes: - docker_certs:/certs forgejo: image: codeberg.org/forgejo/forgejo:1.21 - hostname: forgejo - volumes: - - /srv/forgejo-data:/data - ports: - - 8080:3000 - environment: - FORGEJO__security__INSTALL_LOCK: "true" # remove in production - FORGEJO__log__LEVEL: "debug" # remove in production - FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true" # enables the ability to create a repo when pushing - FORGEJO__repository__DEFAULT_PUSH_CREATE_PRIVATE: "false" # defaults above to public - FORGEJO__repository__DEFAULT_REPO_UNITS: "repo.code,repo.actions" - # `command` is not neecessary, but can be used to create an admin user as shown below when combined with INSTALL_LOCK command: >- bash -c ' /bin/s6-svscan /etc/s6 & sleep 10 ; - su -c "forgejo admin user create --admin --username root --password ROOT_PASSWORD --email root@example.com" git ; su -c "forgejo forgejo-cli actions register --secret {SHARED_SECRET}" git ; + su -c "forgejo admin user create --admin --username root --password {ROOT_PASSWORD} --email root@example.com" git ; sleep infinity ' - - # all values that have defaults listed are optional - # only FORGEJO_SECRET or RUNNER_TOKEN is required, the secret will be prioritized - # FORGEJO_URL is required if forgejo is not in this compose file or docker network - runner-daemon: - ## TODO: Update image to the the release - ## made from this PR: https://code.forgejo.org/forgejo/runner/pulls/283 - - # image: code.forgejo.org/forgejo/runner:3.4.1 - build: ../../ - user: "1000" # defaults to 1000, - restart: unless-stopped # needed for fixing file ownership on restart + environment: + FORGEJO__security__INSTALL_LOCK: "true" + FORGEJO__log__LEVEL: "debug" + FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true" + FORGEJO__repository__DEFAULT_PUSH_CREATE_PRIVATE: "false" + FORGEJO__repository__DEFAULT_REPO_UNITS: "repo.code,repo.actions" volumes: - - /srv/runner-data:/data - - docker_certs:/certs - depends_on: - - docker-in-docker - - forgejo + - /srv/forgejo-data:/data + ports: + - 8080:3000 + + runner-register: + image: code.forgejo.org/forgejo/runner:3.4.1 links: - docker-in-docker - forgejo environment: - FORGEJO_URL: ${FORGEJO_URL} # defaults to http://forgejo:3000 - FORGEJO_SECRET: "{SHARED_SECRET}" # shared secret, must match Forgejo's, overrides RUNNER_TOKEN - RUNNER_TOKEN: ${RUNNER_TOKEN} # token obtained from Forgejo web interface + DOCKER_HOST: tcp://docker-in-docker:2376 + volumes: + - /srv/runner-data:/data + user: 0:0 + command: >- + bash -ec ' + while : ; do + forgejo-runner create-runner-file --connect --instance http://forgejo:3000 --name runner --secret {SHARED_SECRET} && break ; + sleep 1 ; + done ; + sed -i -e "s|\"labels\": null|\"labels\": [\"docker:docker://code.forgejo.org/oci/node:20-bookworm\", \"ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04\"]|" .runner ; + forgejo-runner generate-config > config.yml ; + sed -i -e "s|network: .*|network: host|" config.yml ; + sed -i -e "s|^ envs:$$| envs:\n DOCKER_HOST: tcp://docker:2376\n DOCKER_TLS_VERIFY: 1\n DOCKER_CERT_PATH: /certs/client|" config.yml ; + sed -i -e "s|^ options:| options: -v /certs/client:/certs/client|" config.yml ; + sed -i -e "s| valid_volumes: \[\]$$| valid_volumes:\n - /certs/client|" config.yml ; + chown -R 1000:1000 /data + ' - # Docker Daemon Configs, needed for docker-in-docker + runner-daemon: + image: code.forgejo.org/forgejo/runner:3.4.1 + links: + - docker-in-docker + - forgejo + environment: DOCKER_HOST: tcp://docker:2376 - DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: /certs/client - - # Runner Configs - RUNNER__log__LEVEL: "debug" - RUNNER__container__PRIVILEGED: "true" - RUNNER__runner__LABELS: | - docker:docker://code.forgejo.org/oci/node:20-bookworm - ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04 - DEBUG: "true" - SKIP_WAIT: "false" + DOCKER_TLS_VERIFY: "1" + volumes: + - /srv/runner-data:/data + - docker_certs:/certs + command: >- + bash -c ' + while : ; do test -w .runner && forgejo-runner --config config.yml daemon ; sleep 1 ; done + ' diff --git a/internal/pkg/config/config-env.go b/internal/pkg/config/config-env.go deleted file mode 100644 index 3937a62..0000000 --- a/internal/pkg/config/config-env.go +++ /dev/null @@ -1,156 +0,0 @@ -package config - -import ( - "os" - "strconv" - "strings" - "time" - - log "github.com/sirupsen/logrus" -) - -// loadEnvVars loads configuration settings from environment variables -// prefixed with "RUNNER__" and updates the provided Config struct accordingly. -func loadEnvVars(config *Config) { - // There probably is a better way to do this, but I'm not sure how to do it. - // This implementation causes env-vars to override config file settings, - // without writing to the config file. - - if debug, ok := os.LookupEnv("DEBUG"); ok && debug == "true" { - log.SetLevel(log.DebugLevel) - log.Debug("Debug logging enabled") - } - - // Log - loadEnvStr(config, "RUNNER__log__LEVEL", &config.Log.Level) - loadEnvStr(config, "RUNNER__log__JOB_LEVEL", &config.Log.JobLevel) - - // Runner - loadEnvStr(config, "RUNNER__runner__FILE", &config.Runner.File) - loadEnvInt(config, "RUNNER__runner__CAPACITY", &config.Runner.Capacity) - loadEnvTable(config, "RUNNER__runner__ENVS", &config.Runner.Envs) - loadEnvStr(config, "RUNNER__runner__ENV_FILE", &config.Runner.EnvFile) - loadEnvDuration(config, "RUNNER__runner__SHUTDOWN_TIMEOUT", &config.Runner.ShutdownTimeout) - loadEnvBool(config, "RUNNER__runner__INSECURE", &config.Runner.Insecure) - loadEnvDuration(config, "RUNNER__runner__FETCH_TIMEOUT", &config.Runner.FetchTimeout) - loadEnvDuration(config, "RUNNER__runner__FETCH_INTERVAL", &config.Runner.FetchInterval) - loadEnvDuration(config, "RUNNER__runner__REPORT_INTERVAL", &config.Runner.ReportInterval) - loadEnvList(config, "RUNNER__runner__LABELS", &config.Runner.Labels) - - // Cache - loadEnvBool(config, "RUNNER__cache__ENABLED", config.Cache.Enabled) - loadEnvStr(config, "RUNNER__cache__DIR", &config.Cache.Dir) - loadEnvStr(config, "RUNNER__cache__HOST", &config.Cache.Host) - loadEnvUInt16(config, "RUNNER__cache__PORT", &config.Cache.Port) - loadEnvStr(config, "RUNNER__cache__EXTERNAL_SERVER", &config.Cache.ExternalServer) - - // Container - loadEnvStr(config, "RUNNER__container__NETWORK", &config.Container.Network) - loadEnvStr(config, "RUNNER__container__NETWORK_MODE", &config.Container.NetworkMode) - loadEnvBool(config, "RUNNER__container__ENABLE_IPV6", &config.Container.EnableIPv6) - loadEnvBool(config, "RUNNER__container__PRIVILEGED", &config.Container.Privileged) - loadEnvStr(config, "RUNNER__container__OPTIONS", &config.Container.Options) - loadEnvStr(config, "RUNNER__container__WORKDIR_PARENT", &config.Container.WorkdirParent) - loadEnvList(config, "RUNNER__container__VALID_VOLUMES", &config.Container.ValidVolumes) - loadEnvStr(config, "RUNNER__container__DOCKER_HOST", &config.Container.DockerHost) - loadEnvBool(config, "RUNNER__container__FORCE_PULL", &config.Container.ForcePull) - - // Host - loadEnvStr(config, "RUNNER__host__WORKDIR_PARENT", &config.Host.WorkdirParent) -} - -// General Coverage Docs for below: -// loadEnvType, where Type is the type of the variable being loaded, loads an environment variable into the provided pointer if the environment variable exists and is not empty. -// The key parameter is the environment variable key to look for. -// The dest parameter is a pointer to the variable to load the environment variable into. -// Config is present but unused, it remains for future use to prevent the need to redo the above functions. - -func loadEnvStr(config *Config, key string, dest *string) { - // Example: RUNNER__LOG__LEVEL = "info" - if v := os.Getenv(key); v != "" { - log.Debug("Loading env var: ", key, "=", v) - *dest = v - } -} - -// loadEnvInt loads an environment variable into the provided int pointer if it exists. -func loadEnvInt(config *Config, key string, dest *int) { - // Example: RUNNER__RUNNER__CAPACITY = "1" - if v := os.Getenv(key); v != "" { - if intValue, err := strconv.Atoi(v); err == nil { - log.Debug("Loading env var: ", key, "=", v) - *dest = intValue - } - } -} - -// loadEnvUInt16 loads an environment variable into the provided uint16 pointer if it exists. -func loadEnvUInt16(config *Config, key string, dest *uint16) { - // Example: RUNNER__CACHE__PORT = "8080" - if v := os.Getenv(key); v != "" { - if uint16Value, err := strconv.ParseUint(v, 10, 16); err == nil { - log.Debug("Loading env var: ", key, "=", v) - *dest = uint16(uint16Value) - } - } -} - -// loadEnvDuration loads an environment variable into the provided time.Duration pointer if it exists. -func loadEnvDuration(config *Config, key string, dest *time.Duration) { - // Example: RUNNER__RUNNER__SHUTDOWN_TIMEOUT = "3h" - if v := os.Getenv(key); v != "" { - if durationValue, err := time.ParseDuration(v); err == nil { - log.Debug("Loading env var: ", key, "=", v) - *dest = durationValue - } - } -} - -// loadEnvBool loads an environment variable into the provided bool pointer if it exists. -func loadEnvBool(config *Config, key string, dest *bool) { - // Example: RUNNER__RUNNER__INSECURE = "false" - if v := os.Getenv(key); v != "" { - if boolValue, err := strconv.ParseBool(v); err == nil { - log.Debug("Loading env var: ", key, "=", v) - *dest = boolValue - } - } -} - -// loadEnvTable loads an environment variable into the provided map[string]string pointer if it exists. -func loadEnvTable(config *Config, key string, dest *map[string]string) { - // Example: RUNNER__RUNNER__ENVS = "key1=value1, key2=value2, key3=value3" - if v := os.Getenv(key); v != "" { - *dest = make(map[string]string) - for _, pair := range splitAndTrim(v) { - kv := strings.SplitN(pair, "=", 2) - if len(kv) == 2 { - (*dest)[kv[0]] = kv[1] - } - } - } -} - -// loadEnvList loads an environment variable into the provided []string pointer if it exists. -func loadEnvList(config *Config, key string, dest *[]string) { - // Example: RUNNER__RUNNER__LABELS = "label1, label2, label3" - if v := os.Getenv(key); v != "" { - log.Debug("Loading env var: ", key, "=", v) - *dest = splitAndTrim(v) - } -} - -func splitAndTrim(s string) []string { - lines := strings.Split(s, "\n") - var result []string - for _, line := range lines { - items := strings.Split(line, ",") - for _, item := range items { - trimmed := strings.TrimSpace(item) - if trimmed != "" { - result = append(result, trimmed) - } - } - } - return result -} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 80508de..60be651 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -170,18 +170,5 @@ func LoadDefault(file string) (*Config, error) { } } - // Load environment variables at the end to allow for overriding the default values. - loadEnvVars(cfg) - - // Write the state of the config, including default values, to a debug file. - debugFile := file + ".debug.yml" - content, err := yaml.Marshal(cfg) - if err != nil { - return nil, fmt.Errorf("marshal config to debug file %q: %w", debugFile, err) - } - if err := os.WriteFile(debugFile, content, 0644); err != nil { - return nil, fmt.Errorf("write debug config file %q: %w", debugFile, err) - } - return cfg, nil }