question: is the runner cache really invalidated when restarting runner with no explicit secret? #1041

Closed
opened 2025-10-01 14:29:00 +00:00 by 2franix · 5 comments
Contributor

The documentation states about the cache.secret option:

The shared cache secret used to secure the communications between
the cache proxy and the cache server.
If empty, it will be generated to a new secret automatically when
the server starts and it will stay the same until it restarts.
Every time the secret is modified, all cache entries that were
created with it are invalidated. In order to ensure that the cache
content is reused when the runner restarts, this secret must be
set, for instance with the output of openssl rand -hex 40.

I have configured a v11.1.1 runner with cache.secret left to its default value "". Since the secret is empty I expected the cache to be invalidated every time I restart the runner. By invalidated, I expect the cache contents to be unusable and the runner to behave as if the cache had never been populated.

But this is not what I experienced with a simple workflow doing a rust build:

jobs:
  build:
    name: Build and test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: target/
          key: target
      - name: Build and test
        run: |
          cargo test --all-targets --release

Here are the steps I followed:

  • I recursively deleted .cache/actcache/* while the runner was not running to make sure the cache is empty
  • I ran the workflow once. It took ~1min30s to complete
  • I ran it again. It took ~10s to complete and the cache action stated that cached data were found. So far, so good.
  • I restarted the runner.
  • I ran the workflow again: It took ~10s to complete and the output of the cache action was no different from the previous run.

So obviously, the cache was not invalidated. I digged into the code and confirmed that a new secret is indeed generated upon each startup of the runner. But I could not find anything meaningful relating to the use of this secret to actually encrypt the cache.

It's likely that I am mistaken but cannot figure out where. I'd be very grateful if anyone could shed some light on what looks like a mystery to me. Thanks!

The [documentation](https://forgejo.org/docs/latest/admin/actions/runner-installation/#configuration) states about the `cache.secret` option: > The shared cache secret used to secure the communications between > the cache proxy and the cache server. > If empty, it will be generated to a new secret automatically when > the server starts and it will stay the same until it restarts. > Every time the secret is modified, all cache entries that were > created with it are invalidated. In order to ensure that the cache > content is reused when the runner restarts, this secret must be > set, for instance with the output of openssl rand -hex 40. I have configured a v11.1.1 runner with `cache.secret` left to its default value `""`. Since the secret is empty I expected the cache to be invalidated every time I restart the runner. By _invalidated_, I expect the cache contents to be unusable and the runner to behave as if the cache had never been populated. But this is not what I experienced with a simple workflow doing a rust build: ``` jobs: build: name: Build and test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: target/ key: target - name: Build and test run: | cargo test --all-targets --release ``` Here are the steps I followed: - I recursively deleted `.cache/actcache/*` while the runner was not running to make sure the cache is empty - I ran the workflow once. It took ~1min30s to complete - I ran it again. It took ~10s to complete and the cache action stated that cached data were found. So far, so good. - I restarted the runner. - I ran the workflow again: It took ~10s to complete and the output of the cache action was no different from the previous run. So obviously, the cache was **not** invalidated. I digged into the code and confirmed that a new secret is indeed generated upon each startup of the runner. But I could not find anything meaningful relating to the use of this secret to actually encrypt the cache. It's likely that I am mistaken but cannot figure out where. I'd be very grateful if anyone could shed some light on what looks like a mystery to me. Thanks!
2franix changed title from Is the runner cache _really_ invalidated when restarting runner with no explicit secret? to Is the runner cache really invalidated when restarting runner with no explicit secret? 2025-10-01 14:29:37 +00:00
2franix changed title from Is the runner cache really invalidated when restarting runner with no explicit secret? to question: is the runner cache really invalidated when restarting runner with no explicit secret? 2025-10-01 14:30:39 +00:00
Contributor

could you please provide the config.yml you are using?

could you please provide the `config.yml` you are using?
Author
Contributor

Sure, here you go:

# Example configuration file, it's safe to copy this as the default config file without any modification.
# You don't have to copy this file to your instance,
# just run `./act_runner generate-config > config.yaml` to generate a config file.

log:
  # The level of logging, can be trace, debug, info, warn, error, fatal
  level: info
runner:
  # Where to store the registration result.
  file: .runner
  # Execute how many tasks concurrently at the same time.
  capacity: 2
  # Extra environment variables to run jobs.
  envs:
  #  A_TEST_ENV_NAME_1: a_test_env_value_1
  #  A_TEST_ENV_NAME_2: a_test_env_value_2
  # Extra environment variables to run jobs from a file.
  # It will be ignored if it's empty or the file doesn't exist.
  env_file: .env
  # The timeout for a job to be finished.
  # Please note that the Gitea instance also has a timeout (3h by default) for the job.
  # So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
  timeout: 3h
  # Whether skip verifying the TLS certificate of the Gitea instance.
  insecure: false
  # The timeout for fetching the job from the Gitea instance.
  fetch_timeout: 5s
  # The interval for fetching the job from the Gitea instance.
  fetch_interval: 2s
  # The labels of a runner are used to determine which jobs the runner can run, and how to run them.
  # Like: ["macos-arm64:host", "ubuntu-latest:docker://node:16-bullseye", "ubuntu-22.04:docker://node:16-bullseye"]
  # If it's empty when registering, it will ask for inputting labels.
  # If it's empty when execute `deamon`, will use labels in `.runner` file.
  labels:
    # See https://github.com/nektos/act/blob/master/IMAGES.md
    - ubuntu-latest:docker:ghcr.io/catthehacker/ubuntu:rust-latest
    - ubuntu-latest-prosper:docker:ghcr.io/catthehacker/ubuntu:rust-latest
cache:
  # Enable cache server to use actions/cache.
  enabled: true
  # The directory to store the cache data.
  # If it's empty, the cache data will be stored in $HOME/.cache/actcache.
  dir: ""
  # The host of the cache server.
  # It's not for the address to listen, but the address to connect from job containers.
  # So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
  host: ""
  # The port of the cache server.
  # 0 means to use a random available port.
  port: 0
  # The external cache server URL. Valid only when enable is true.
  # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
  # The URL should generally end with "/".
  external_server: ""
container:
  # Specifies the network to which the container will connect.
  # Could be host, bridge or the name of a custom network.
  # If it's empty, act_runner will create a network automatically.
  network:
  # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
  privileged: false
  # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
  options: --env DOCKER_HOST=tcp://dind --add-host dind:host-gateway --env FORGEJO_DOMAIN --cpu-shares 256
  # The parent directory of a job's working directory.
  # If it's empty, /workspace will be used.
  workdir_parent:
  # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
  # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
  # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
  # valid_volumes:
  #   - data
  #   - /src/*.json
  # If you want to allow any volume, please use the following configuration:
  # valid_volumes:
  #   - '**'
  valid_volumes: []
  # overrides the docker client host with the specified one.
  # If it's empty, act_runner will find an available docker host automatically.
  # If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
  # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
  docker_host: ""
  # Pull docker image(s) even if already present
  force_pull: false
host:
  # The parent directory of a job's working directory.
  # If it's empty, $HOME/.cache/act/ will be used.
  workdir_parent:

I created it about a year ago so the template looks a bit outdated.

Sure, here you go: ```yaml # Example configuration file, it's safe to copy this as the default config file without any modification. # You don't have to copy this file to your instance, # just run `./act_runner generate-config > config.yaml` to generate a config file. log: # The level of logging, can be trace, debug, info, warn, error, fatal level: info runner: # Where to store the registration result. file: .runner # Execute how many tasks concurrently at the same time. capacity: 2 # Extra environment variables to run jobs. envs: # A_TEST_ENV_NAME_1: a_test_env_value_1 # A_TEST_ENV_NAME_2: a_test_env_value_2 # Extra environment variables to run jobs from a file. # It will be ignored if it's empty or the file doesn't exist. env_file: .env # The timeout for a job to be finished. # Please note that the Gitea instance also has a timeout (3h by default) for the job. # So the job could be stopped by the Gitea instance if it's timeout is shorter than this. timeout: 3h # Whether skip verifying the TLS certificate of the Gitea instance. insecure: false # The timeout for fetching the job from the Gitea instance. fetch_timeout: 5s # The interval for fetching the job from the Gitea instance. fetch_interval: 2s # The labels of a runner are used to determine which jobs the runner can run, and how to run them. # Like: ["macos-arm64:host", "ubuntu-latest:docker://node:16-bullseye", "ubuntu-22.04:docker://node:16-bullseye"] # If it's empty when registering, it will ask for inputting labels. # If it's empty when execute `deamon`, will use labels in `.runner` file. labels: # See https://github.com/nektos/act/blob/master/IMAGES.md - ubuntu-latest:docker:ghcr.io/catthehacker/ubuntu:rust-latest - ubuntu-latest-prosper:docker:ghcr.io/catthehacker/ubuntu:rust-latest cache: # Enable cache server to use actions/cache. enabled: true # The directory to store the cache data. # If it's empty, the cache data will be stored in $HOME/.cache/actcache. dir: "" # The host of the cache server. # It's not for the address to listen, but the address to connect from job containers. # So 0.0.0.0 is a bad choice, leave it empty to detect automatically. host: "" # The port of the cache server. # 0 means to use a random available port. port: 0 # The external cache server URL. Valid only when enable is true. # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself. # The URL should generally end with "/". external_server: "" container: # Specifies the network to which the container will connect. # Could be host, bridge or the name of a custom network. # If it's empty, act_runner will create a network automatically. network: # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker). privileged: false # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway). options: --env DOCKER_HOST=tcp://dind --add-host dind:host-gateway --env FORGEJO_DOMAIN --cpu-shares 256 # The parent directory of a job's working directory. # If it's empty, /workspace will be used. workdir_parent: # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted. # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to: # valid_volumes: # - data # - /src/*.json # If you want to allow any volume, please use the following configuration: # valid_volumes: # - '**' valid_volumes: [] # overrides the docker client host with the specified one. # If it's empty, act_runner will find an available docker host automatically. # If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers. # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work. docker_host: "" # Pull docker image(s) even if already present force_pull: false host: # The parent directory of a job's working directory. # If it's empty, $HOME/.cache/act/ will be used. workdir_parent: ``` I created it about a year ago so the template looks a bit outdated.
Contributor

It is error on my part. Back when I wrote this part of the documentation I was somehow mistaken into thinking the secret played a part in determining the location of the cache file in the cache directory. But after double checking and reproducing the behavior you described I confirm it was just not true.

Note that the secret is used to secure the communications between the cache and the cache proxy. It is not used to encrypt the cache files, they are stored unencrypted.

It is error on my part. Back when I wrote this part of the documentation I was somehow mistaken into thinking the secret played a part in determining the location of the cache file in the cache directory. But after double checking and reproducing the behavior you described I confirm it was just not true. Note that the secret is used to secure the communications between the cache and the cache proxy. It is not used to encrypt the cache files, they are stored unencrypted.
Author
Contributor

Ah, thanks a lot for investigating that so quickly! Your assessment of the code aligns more with what I read and understood as well.

As per the cache encryption, I did not find any traces of it in the code but since there was this invalidation thing described in the doc, that was my assumption that there had to be one. Thanks for pointing that out!

Is there still any benefit from setting an explicit secret, then? It looks like the default behavior would be good enough in all situations but I may miss something.

Thanks again, mystery solved :)

Ah, thanks a lot for investigating that so quickly! Your assessment of the code aligns more with what I read and understood as well. As per the cache encryption, I did not find any traces of it in the code but since there was this invalidation thing described in the doc, that was my assumption that there had to be one. Thanks for pointing that out! Is there still any benefit from setting an explicit secret, then? It looks like the default behavior would be good enough in all situations but I may miss something. Thanks again, mystery solved :)
Contributor

Is there still any benefit from setting an explicit secret, then? It looks like the default behavior would be good enough in all situations but I may miss something.

If not using an external server, I agree: there is no benefit in setting a secret.

> Is there still any benefit from setting an explicit secret, then? It looks like the default behavior would be good enough in all situations but I may miss something. If not using an external server, I agree: there is no benefit in setting a secret.
Sign in to join this conversation.
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
forgejo/runner#1041
No description provided.