security: fix log/state file permissions, Firebase key on disk, TLS cleanup

- agent_loop.py: create log dir with mode 0700 and enforce it on
  existing dirs; open log files with mode 0600; chmod state file
  to 0600 after every write. Prevents other local processes from
  reading agent output (which may contain credential paths) or
  tampering with the state file's pid field.

- ci/main.go (TestAndroidFirebase): replace
    echo "$FIREBASE_SA_KEY" > /tmp/key.json
  with bash process substitution
    --key-file=<(echo "$FIREBASE_SA_KEY")
  The key is now passed via a file descriptor — it never touches
  disk, so it cannot be stranded by a failed gcloud auth call or
  snapshotted into the Dagger layer cache.

- ci.yml / deploy.yml: add "Cleanup TLS credentials" step
  (if: always()) at the end of every job that calls
  setup_dagger_remote.sh. Removes /tmp/dagger-tls,
  /tmp/stunnel-dagger.conf, /tmp/stunnel.pid from the self-hosted
  runner after each job, so client certs do not accumulate between
  job runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-23 10:54:53 +02:00
co-authored by Claude Sonnet 4.6
parent 509a0bc954
commit b6a2f91820
4 changed files with 25 additions and 5 deletions
+4
View File
@@ -34,3 +34,7 @@ jobs:
env: env:
DAGGER_NO_NAG: "1" DAGGER_NO_NAG: "1"
run: task check-dagger run: task check-dagger
- name: Cleanup TLS credentials
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
+16
View File
@@ -37,6 +37,10 @@ jobs:
DAGGER_NO_NAG: "1" DAGGER_NO_NAG: "1"
run: task test-android-firebase run: task test-android-firebase
- name: Cleanup TLS credentials
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
deploy-playstore: deploy-playstore:
name: Build & Deploy to Play Store name: Build & Deploy to Play Store
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -80,6 +84,10 @@ jobs:
DAGGER_NO_NAG: "1" DAGGER_NO_NAG: "1"
run: task deploy-apk run: task deploy-apk
- name: Cleanup TLS credentials
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
build-linux: build-linux:
name: Build Linux Release name: Build Linux Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -113,6 +121,10 @@ jobs:
DAGGER_NO_NAG: "1" DAGGER_NO_NAG: "1"
run: task deploy-linux run: task deploy-linux
- name: Cleanup TLS credentials
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
publish-website: publish-website:
name: Publish Website Build History name: Publish Website Build History
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -150,6 +162,10 @@ jobs:
DAGGER_NO_NAG: "1" DAGGER_NO_NAG: "1"
run: task publish-website run: task publish-website
- name: Cleanup TLS credentials
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
label-deploy-health: label-deploy-health:
name: Update Deploy Health Label name: Update Deploy Health Label
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1 -3
View File
@@ -680,10 +680,8 @@ func (m *Ci) TestAndroidFirebase(
WithEnvVariable("FIREBASE_PROJECT_ID", projectID). WithEnvVariable("FIREBASE_PROJECT_ID", projectID).
WithExec([]string{"/bin/bash", "-c", WithExec([]string{"/bin/bash", "-c",
`auth_err=$(mktemp); trap 'rm -f "$auth_err"' EXIT; \ `auth_err=$(mktemp); trap 'rm -f "$auth_err"' EXIT; \
echo "$FIREBASE_SA_KEY" > /tmp/key.json; \ gcloud auth activate-service-account --key-file=<(echo "$FIREBASE_SA_KEY") 2>"$auth_err" \
gcloud auth activate-service-account --key-file=/tmp/key.json 2>"$auth_err" \
|| { cat "$auth_err"; exit 1; }; \ || { cat "$auth_err"; exit 1; }; \
rm -f /tmp/key.json; \
gcloud config set project "$FIREBASE_PROJECT_ID" 2>>"$auth_err" \ gcloud config set project "$FIREBASE_PROJECT_ID" 2>>"$auth_err" \
|| { cat "$auth_err"; exit 1; }; \ || { cat "$auth_err"; exit 1; }; \
unknown=$(grep -vF "Activated service account credentials for:" "$auth_err" \ unknown=$(grep -vF "Activated service account credentials for:" "$auth_err" \
+4 -2
View File
@@ -205,6 +205,7 @@ def _write_state(pid: int | None, issue: int | None, kind: str, issue_title: str
if ci_run_id is not None: if ci_run_id is not None:
data["ci_run_id_at_start"] = ci_run_id data["ci_run_id_at_start"] = ci_run_id
STATE_FILE.write_text(json.dumps(data, indent=2)) STATE_FILE.write_text(json.dumps(data, indent=2))
STATE_FILE.chmod(0o600)
def _clear_state() -> None: def _clear_state() -> None:
@@ -217,11 +218,12 @@ def _clear_state() -> None:
def _start_agent(prompt: str, session_name: str) -> int: def _start_agent(prompt: str, session_name: str) -> int:
"""Start Claude Code as a detached background process and return its PID.""" """Start Claude Code as a detached background process and return its PID."""
log_dir = Path.home() / ".sharedinbox-agent-logs" log_dir = Path.home() / ".sharedinbox-agent-logs"
log_dir.mkdir(exist_ok=True) log_dir.mkdir(mode=0o700, exist_ok=True)
log_dir.chmod(0o700) # fix permissions if dir already existed with wrong mode
ts = datetime.now().strftime("%Y%m%dT%H%M%S") ts = datetime.now().strftime("%Y%m%dT%H%M%S")
log_file = log_dir / f"{session_name}-{ts}.log" log_file = log_dir / f"{session_name}-{ts}.log"
log_fh = open(log_file, "w") log_fh = open(log_file, "w", opener=lambda p, f: os.open(p, f, 0o600))
proc = subprocess.Popen( proc = subprocess.Popen(
[ [
"claude", "claude",