diff --git a/.changeset/quiet-gitlab-credentials.md b/.changeset/quiet-gitlab-credentials.md new file mode 100644 index 00000000..ccad3782 --- /dev/null +++ b/.changeset/quiet-gitlab-credentials.md @@ -0,0 +1,5 @@ +--- +"openwiki": patch +--- + +Use an ephemeral credential helper in the GitLab CI example so push credentials cannot persist in remote URLs or appear under shell tracing. diff --git a/examples/openwiki-update.gitlab-ci.yml b/examples/openwiki-update.gitlab-ci.yml index d971dcaa..a4b7ebe5 100644 --- a/examples/openwiki-update.gitlab-ci.yml +++ b/examples/openwiki-update.gitlab-ci.yml @@ -21,7 +21,26 @@ openwiki_update: - git checkout -b "$OPENWIKI_BRANCH" - git add openwiki AGENTS.md CLAUDE.md .github/workflows/openwiki-update.yml - git commit -m "docs: update OpenWiki" - - git push "https://oauth2:${OPENWIKI_GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "$OPENWIKI_BRANCH" + - | + # Configure OPENWIKI_GITLAB_TOKEN as a protected, masked CI/CD variable. + # Keep tracing disabled while the temporary helper can read the token. + set +x + credential_helper="$(mktemp)" + trap 'rm -f "$credential_helper"' EXIT + chmod 700 "$credential_helper" + cat > "$credential_helper" <<'EOF' + #!/bin/sh + case "$1" in + get) + printf 'username=%s\n' oauth2 + printf 'password=%s\n' "$OPENWIKI_GITLAB_TOKEN" + ;; + esac + EOF + git -c credential.helper= -c credential.helper="$credential_helper" \ + push "https://${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "$OPENWIKI_BRANCH" + rm -f "$credential_helper" + trap - EXIT - | curl --fail --request POST \ --header "PRIVATE-TOKEN: ${OPENWIKI_GITLAB_TOKEN}" \ diff --git a/test/gitlab-example-security.test.ts b/test/gitlab-example-security.test.ts new file mode 100644 index 00000000..afffdfcf --- /dev/null +++ b/test/gitlab-example-security.test.ts @@ -0,0 +1,27 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, test } from "vitest"; +import { parse } from "yaml"; + +const examplePath = new URL( + "../examples/openwiki-update.gitlab-ci.yml", + import.meta.url, +); + +describe("GitLab CI example", () => { + test("keeps Git credentials out of remote URLs and traced commands", async () => { + const example = await readFile(examplePath, "utf8"); + const config = parse(example) as { + openwiki_update?: { script?: unknown[] }; + }; + + expect(config.openwiki_update?.script).toBeInstanceOf(Array); + expect(example).not.toMatch(/https?:\/\/[^/\s]+@/u); + expect(example).toContain("set +x"); + expect(example).toContain('credential.helper="$credential_helper"'); + expect(example).toContain("<<'EOF'"); + expect(example).toContain('"$OPENWIKI_GITLAB_TOKEN"'); + expect(example).toContain( + 'push "https://${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"', + ); + }); +});