#Use secret refs for sandbox.docker.env

34 messages · Page 1 of 1 (latest)

snow scroll
#

Hi!

Do you know a workaround not to put my Linear API key as a plaintext in the sandbox.docker.env property of my openclaw.json file?
Thank you!

final mirageBOT
#

Short answer: not as a first-class feature today.

I checked the current docs/source, and agents.defaults.sandbox.docker.env is not on the supported SecretRef surface, so the secrets system does not currently manage that field the same way it does provider API keys, gateway auth, or sandbox SSH material.

Docs:

So the practical workarounds are:

  1. Best if possible: avoid putting the Linear key in sandbox.docker.env at all, and run that integration on the host side where a SecretRef-supported field exists.
  2. If it must stay sandboxed: keep the secret outside openclaw.json in a separate secret store/file, then mount it read-only into the sandbox or use a custom sandbox image/entrypoint that reads it at runtime.

I would not rely on "${LINEAR_API_KEY}" inside sandbox.docker.env as a supported SecretRef pattern here.

If you want, I can sketch a minimal read-only bind or custom-image setup for Linear.

snow scroll
#

Yes let's do this

final mirageBOT
#

Yep, the least ugly workaround I’d use is:

mount the secret as a read-only file, then have the sandbox load it from /etc/profile.d instead of storing it in sandbox.docker.env.

1) Put the Linear key in a host file, outside your repo

Use a dedicated path, not ~/.ssh / ~/.config style dirs.

sudo install -d -m 700 /opt/openclaw-secrets
printf '%s' 'lin_api_xxx' | sudo tee /opt/openclaw-secrets/linear_api_key >/dev/null
sudo chmod 600 /opt/openclaw-secrets/linear_api_key

2) Add this to openclaw.json (JSON5)

{
#
  agents: {
    defaults: {
      sandbox: {
        docker: {
          binds: [
            "/opt/openclaw-secrets/linear_api_key:/run/secrets/linear_api_key:ro",
          ],
          readOnlyRoot: false,
          setupCommand: [
            "mkdir -p /etc/profile.d",
            "cat >/etc/profile.d/linear.sh <<'EOF'",
            "if [ -f /run/secrets/linear_api_key ]; then",
            "  export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
            "fi",
            "EOF",
#
            "chmod 0644 /etc/profile.d/linear.sh"
          ]
        }
      }
    }
  }
}

Why this works:

  • the secret is not in openclaw.json
  • it is mounted read-only
  • OpenClaw sandbox execs use a login shell, so /etc/profile.d/linear.sh gets loaded on future turns

3) Recreate the sandbox so setupCommand runs again

#
openclaw sandbox recreate --all

Docs:

Small caveat

If your sandbox image is non-root, setupCommand may hit a permission error writing /etc/profile.d. If that happens, the cleaner long-term version is a custom sandbox image with that linear.sh file baked in.

If you want, I can also give you the custom image version, which is cleaner than readOnlyRoot: false.

snow scroll
#

agents: {
defaults: {
sandbox: {
docker: {
binds: [
"/opt/openclaw-secrets/linear_api_key:/run/secrets/linear_api_key:ro",
],
readOnlyRoot: false,
setupCommand: [
"mkdir -p /etc/profile.d",
"cat >/etc/profile.d/linear.sh <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"",
"fi",
"EOF",
"chmod 0644 /etc/profile.d/linear.sh"
]
}
}
}
}
}

can't I just have a setup command that directly export using cat?

final mirageBOT
#

Not quite.

setupCommand runs once when the sandbox container is created. So if you do:

export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"

that only affects that one setup shell. Later OpenClaw turns run fresh shells inside the container, so they will not inherit that export.

So the rule is:

  • direct export in setupCommand → no, not persistent
  • write a file that future login shells source (/etc/profile.d/...) → yes
  • start one long-lived service from setupCommand with the env inline → yes, but only for that service

For example, this works only if you launch the actual process there:

#
LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)" your-command-here

If you want the env available for normal future sandbox execs, the /etc/profile.d/linear.sh trick is the right one.

If your Linear tool supports something like LINEAR_API_KEY_FILE, that’s even cleaner, because then you can just mount the file and skip the export entirely.

snow scroll
#

/.openclaw/workspace). Use a dangerous override only when you fully trust this runtime.
Apr 15 17:20:20 ip-172-26-7-36 node[364271]: 2026-04-15T17:20:20.470+00:00 [diagnostic] lane task error: lane=main durationMs=2016 error="Error: Sandbox security: bind mount "/home/ubuntu/linear_api_key:/run/secrets/linear_api_key:ro" source "/home/ubuntu/linear_api_key" is outside allowed roots (/home/ubuntu/.openclaw/sandboxes/agent-main-slack-channel-c0aszs4-ea97881d, /home/ubuntu/.openclaw/workspace). Use a dangerous override only when you fully trust this runtime."

final mirageBOT
#

Yep, that error is the sandbox guardrail doing its job.

By default, bind sources are only allowed from the workspace / agent workspace roots. Your file is in /home/ubuntu/linear_api_key, so it gets rejected as an external bind source.

If this is your own trusted runtime, the fix is:

agents: {
  defaults: {
    sandbox: {
      docker: {
        binds: [
          "/home/ubuntu/linear_api_key:/run/secrets/linear_api_key:ro",
        ],
        dangerouslyAllowExternalBindSources: true,
        readOnlyRoot: false,
#
        setupCommand: [
          "mkdir -p /etc/profile.d",
          "cat >/etc/profile.d/linear.sh <<'EOF'",
          "if [ -f /run/secrets/linear_api_key ]; then",
          "  export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
          "fi",
          "EOF",
          "chmod 0644 /etc/profile.d/linear.sh"
        ]
      }
    }
  }
}

Then recreate the sandbox:

#
openclaw sandbox recreate --all

Docs: https://docs.openclaw.ai/gateway/sandboxing

My recommendation:

  • If this is a personal/private agent: this override is reasonable.
  • If this agent handles untrusted stuff: better move the secret file under an allowed root instead, rather than opening external bind sources globally.

If you want, I can show the per-agent version so you don’t enable that override for every sandbox.

snow scroll
#

the let's create linear_api_key inside the agent workspace? what would be the new bind?

final mirageBOT
#

Yep, use the stable workspace root, not the transient ~/.openclaw/sandboxes/agent-* path.

I’d put it here:

/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key

Then the bind becomes:

binds: [
  "/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
]

So your Docker block can be:

#
docker: {
  binds: [
    "/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
  ],
  readOnlyRoot: false,
  setupCommand: [
    "mkdir -p /etc/profile.d",
    "cat >/etc/profile.d/linear.sh <<'EOF'",
    "if [ -f /run/secrets/linear_api_key ]; then",
    "  export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
    "fi",
    "EOF",
    "chmod 0644 /etc/profile.d/linear.sh"
  ]
}
#

Nice part: with that path, you should not need dangerouslyAllowExternalBindSources: true.

I would also make the host file private:

mkdir -p /home/ubuntu/.openclaw/workspace/.secrets
chmod 700 /home/ubuntu/.openclaw/workspace/.secrets
printf '%s' 'lin_api_xxx' > /home/ubuntu/.openclaw/workspace/.secrets/linear_api_key
chmod 600 /home/ubuntu/.openclaw/workspace/.secrets/linear_api_key

Then:

openclaw sandbox recreate --all
#

If this fixes it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow can index it.

snow scroll
#

ane=main durationMs=19370 error="Error: /bin/sh: 2: cannot create /etc/profile.d/linear.sh: Read-only file system
Apr 15 17:28:52 ip-172-26-7-36 node[364271]: chmod: cannot access '/etc/profile.d/linear.sh': No such file or directory"

final mirageBOT
#

Ah, nice catch. /etc/profile.d is on the container rootfs, so if that stays read-only, write it into the writable sandbox home/workdir instead.

OpenClaw sandbox execs set HOME=/workspace and run a login shell, so use:

docker: {
  binds: [
    "/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
  ],
  setupCommand: [
    "cat >/workspace/.profile <<'EOF'",
    "if [ -f /run/secrets/linear_api_key ]; then",
    "  export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
    "fi",
    "EOF",
    "chmod 0600 /workspace/.profile"
#
  ]
}

So yes:

  • new bind stays the same
  • just change the target file from /etc/profile.d/linear.sh to /workspace/.profile
  • you can now remove readOnlyRoot: false

Then recreate the sandbox:

openclaw sandbox recreate --all

If you already have something in /workspace/.profile, use >> instead of > so you append instead of overwrite.

snow scroll
#

"sandbox": { "docker": { "binds": ["/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",], "env": {}, "image": "openclaw-sandbox-common:bookworm-slim", "network": "bridge", "user": "1000:1000", "setupCommand": [ "mkdir -p /etc/profile.d", "cat >/etc/profile.d/linear.sh <<'EOF'", "if [ -f /run/secrets/linear_api_key ]; then", " export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"", "fi", "EOF", "chmod 0644 /etc/profile.d/linear.sh" ] }, "mode": "all", "scope": "session", "workspaceAccess": "ro" }

this is my sandbox config btw

final mirageBOT
#

Yep, with that config the problem is clear:

  • user: "1000:1000" means the setup runs as a non-root user
  • you’re trying to write to /etc/profile.d
  • rootfs is effectively read-only for that path

So /etc/profile.d is the wrong target here.

With workspaceAccess: "ro", the writable sandbox workspace is still /workspace, and the agent workspace is mounted read-only at /agent. So change only the setupCommand to write /workspace/.profile instead:

"sandbox": {
  "docker": {
    "binds": [
      "/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro"
    ],
#
    "env": {},
    "image": "openclaw-sandbox-common:bookworm-slim",
    "network": "bridge",
    "user": "1000:1000",
    "setupCommand": [
      "cat >/workspace/.profile <<'EOF'",
      "if [ -f /run/secrets/linear_api_key ]; then",
      "  export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
      "fi",
      "EOF",
      "chmod 0600 /workspace/.profile"
    ]
  },
  "mode": "all",
  "scope": "session",
#
  "workspaceAccess": "ro"
}

So:

  • keep the bind
  • keep user: "1000:1000"
  • remove the /etc/profile.d bits
  • use /workspace/.profile

Then run:

openclaw sandbox recreate --all
#

I’m pretty confident this is the right shape for your current sandbox config.

snow scroll
#

:34.452+00:00 [diagnostic] lane task error: lane=main durationMs=14074 error="Error: /bin/sh: 1: cannot create /workspace/.profile: Read-only file system
Apr 15 17:35:34 ip-172-26-7-36 node[364271]: chmod: cannot access '/workspace/.profile': No such file or directory"

final mirageBOT
#

Ah, I see it now. I misread the workspaceAccess: "ro" behavior earlier, sorry.

With your config, both of my write-based ideas fail:

  • /etc/profile.d/... fails because you’re non-root and the rootfs is read-only
  • /workspace/.profile fails because with workspaceAccess: "ro", /workspace is mounted read-only too

So the clean fix is: don’t write inside the container at all.
Instead, bind-mount a small helper script from the host straight into /etc/profile.d/linear.sh.

Host files

/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key

lin_api_xxx
#

/home/ubuntu/.openclaw/workspace/.secrets/linear.sh

if [ -f /run/secrets/linear_api_key ]; then
  export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"
fi

New binds

"binds": [
  "/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
  "/home/ubuntu/.openclaw/workspace/.secrets/linear.sh:/etc/profile.d/linear.sh:ro"
]

Then remove setupCommand

#

You shouldn’t need it anymore.

So your Docker config becomes roughly:

"docker": {
  "binds": [
    "/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
    "/home/ubuntu/.openclaw/workspace/.secrets/linear.sh:/etc/profile.d/linear.sh:ro"
  ],
  "env": {},
  "image": "openclaw-sandbox-common:bookworm-slim",
  "network": "bridge",
  "user": "1000:1000"
}
#

Then recreate the sandbox:

openclaw sandbox recreate --all

This should fit your current sandbox config much better, because it avoids all container-internal writes.

snow scroll
#

$ /etc/profile.d/linear.sh
/bin/sh: 2: /etc/profile.d/linear.sh: Permission denied
$

final mirageBOT
#

That specific error is probably just because you’re executing the file, not sourcing it.

/etc/profile.d/linear.sh is meant to be read by the shell during login, so it does not need to be executable.

Good test

/bin/sh -lc 'printf "%s\n" "$LINEAR_API_KEY"'

or manually source it:

. /etc/profile.d/linear.sh
printf "%s\n" "$LINEAR_API_KEY"

Also make sure the host script is readable