Module 6: Supply Chain Sbom And Signing
5. Hands-on: installing `cosign` and a local keypair
Description
With lesson 4's theory resolved, this lesson installs cosign for real, confirms its version, and generates the local keypair lessons 6 and 7 are going to use to sign and verify lambda/function.zip. The step that surprises most people the first time — and the reason this lesson dedicates a full section to it — is that cosign generate-key-pair asks for a password interactively, something that would break any attempt to automate this step inside a CI pipeline. This lesson shows the real mechanism to resolve that without blocking anything, documented with this guide's literal output.
Connection to the module
cosign.key and cosign.pub, generated here, are the two files this module's lessons 6, 7, and 8 are going to use in every signing and verification command. Along with lesson 3's sbom.cyclonedx.json, it's one of the three new artifacts this module adds to andes-cargo-infra/.
Step 1 — Installing cosign
brew install cosign
What to expect (summary — brew's complete output varies depending on what you already had installed; the result that matters is the final binary):
==> Fetching downloads for: cosign
✔︎ Bottle cosign (3.1.3)
==> Would install 1 formula:
cosign
🍺 /opt/homebrew/Cellar/cosign/3.1.3: 12 files, 97.9MB
If you're not on macOS with Homebrew, the official sigstore/cosign releases page has binaries for Linux and Windows — the rest of this lesson, and this entire module, doesn't depend on how you installed the binary, only on cosign being available on your PATH.
Confirm the exact version:
cosign version
What to expect (literal, run to write this lesson):
GitVersion: v3.1.3
GitCommit: 11926fa5bbbbde47e88fc006b625a17769b743b2
GitTreeState: "clean"
BuildDate: 2026-08-05T23:43:27Z
GoVersion: go1.26.5
Compiler: gc
Platform: darwin/arm64
v3.1.3 — the same version confirmed in this guide's design, published August 6, 2026 as a security patch (GHSA-fx35-mq7g-6g98, on handling a legacy bundle format). If your installation reports a higher version, there's no problem: every lesson in this module describes behavior observed with this exact version, and cosign keeps forward compatibility on the commands this module uses.
Step 2 — Generating the keypair, and the real interactive-password problem
The command that generates the key pair is:
cosign generate-key-pair
If you run it exactly like that, in an interactive terminal, you're going to see this:
Enter password for private key:
cosign encrypts the private key (cosign.key) with a password before writing it to disk — an extra layer of protection: if someone steals the cosign.key file without knowing that password, they can't use it to sign anything. It's a reasonable design decision for the tool's normal interactive use. The problem shows up in this exact guide's context — and in any real CI pipeline: an interactive prompt waiting for someone to type something can't run inside an automated script or a GitHub Actions job, where there's no human in front of a terminal to type a password at the exact moment the command asks for it. If you ran this command as-is inside lesson 8 (which integrates it into apply.yml), the job would just hang waiting for input that never arrives, until the runner's timeout expires.
Step 3 — The real solution: the COSIGN_PASSWORD environment variable
cosign recognizes the COSIGN_PASSWORD environment variable and, if it's set — even empty —, uses it as the password without showing any interactive prompt:
COSIGN_PASSWORD="" cosign generate-key-pair
What to expect (literal, run to write this lesson — no prompt at all, no waiting):
Private key written to cosign.key
Public key written to cosign.pub
Two lines, no question, exit code 0. This guide uses an empty password deliberately, for the same honesty reason you already saw with AWS_ACCESS_KEY_ID=test/AWS_SECRET_ACCESS_KEY=test in terraform-and-iac-guide and cicd-and-gitops-on-aws-guide: it's a $0 lab, with no real secrets to protect behind this key — this guide's cosign.key signs an 890-byte practice .zip, not a real company's production artifact. In a real project, COSIGN_PASSWORD would still be the correct mechanism to automate this step, but its value would come from a managed secret — exactly the same SSM Parameter Store/Secrets Manager you built in Module 3, or GitHub Actions' secrets equivalent —, never a hand-written empty string.
Step 4 — Inspecting the two generated files
ls -la cosign.key cosign.pub
What to expect (literal, run to write this lesson):
-rw------- 1 andes-cargo staff 653 cosign.key
-rw-r--r-- 1 andes-cargo staff 178 cosign.pub
Notice the permissions, not just the sizes: cosign.key ends up with 600 permissions (read/write for the owner only) by default — nobody else on the same system can even read the file, a second layer of protection independent of the password that already encrypts its content. cosign.pub, by contrast, ends up with 644 permissions (readable by anyone) — consistent with its purpose: a public key exists, precisely, to be distributed without restriction, exactly the opposite of the private one.
Look at each file's format:
cat cosign.pub
What to expect (literal — the key's mathematical content is unique to each run, but the format is always this):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE42I8G6E1Zbwq+VSyEaL4QM6JU3Si
f6FM6IKkLRBPp0Yjy3wwL1Ehyakktmm9qByYGqQ4sTpj6OQ2OWsjFpLgEw==
-----END PUBLIC KEY-----
A standard PEM block (-----BEGIN PUBLIC KEY-----), the same format any cryptographic tool that works with public keys recognizes — there's nothing Sigstore-proprietary in this file, it's a SubjectPublicKeyInfo for ECDSA over the P-256 curve (prime256v1), cosign generate-key-pair's default algorithm.
head -1 cosign.key
What to expect (literal):
-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----
Different from the generic BEGIN EC PRIVATE KEY header you'd see in a traditional, unencrypted PKI key — ENCRYPTED SIGSTORE PRIVATE KEY confirms, right in the file's own header, that the content is encrypted with the password you set in Step 3, and that the internal format is Sigstore-specific (not a generic PKCS#8), though wrapped in the same recognizable PEM shell.
Step 5 — Making sure cosign.key never gets versioned
cosign.pub is exactly the kind of file you do want in the repository — it's the key anyone needs to verify a signature of yours, and its public exposure compromises nothing. cosign.key, by contrast, even though it's encrypted, should never be versioned: it's a defense layer, not an excuse to lower your guard. Add it to andes-cargo-infra/'s root .gitignore — the same file that already excludes lambda/function.zip, .terraform/, and terraform.tfstate from earlier modules:
echo "cosign.key" >> .gitignore
git status --short
What to expect (literal — assuming cosign.pub does stay to be committed, and cosign.key disappears from the tracked-files list):
M .gitignore
?? cosign.pub
cosign.key doesn't appear at all in git status — exactly the correct behavior of a working .gitignore —, while cosign.pub does appear as a new file, ready for the next commit. This is the same discipline terraform-and-iac-guide already established with function.zip (generated, never versioned) and that Module 3's secrets.tf established with any real secret value: what can be regenerated or what should never be distributed, out of the repository; what others need to verify your work, in.
Common mistakes
Forgetting COSIGN_PASSWORD when automating this step, and having the process hang with no clear error message (this lesson's most important mistake). What happens: someone copies cosign generate-key-pair into a script or a CI job without the environment variable, and execution simply doesn't move forward — no failure, no message, just waiting indefinitely. How to spot it: if a script that should finish in seconds looks "hung," with no new output in the terminal. How to fix it: any invocation of cosign generate-key-pair outside an interactive human session needs COSIGN_PASSWORD set in the environment — empty, like this lesson, or with a real value managed by a secret —, no exceptions.
Accidentally committing cosign.key along with cosign.pub, before configuring .gitignore (sequencing mistake, revisits Module 3). What happens: someone generates the keypair before touching .gitignore, and both files show up as "untracked" in git status — a careless git add . versions both. How to spot it: git log --all --full-history -- cosign.key shows a commit, even if you deleted the file afterward (Git's history doesn't forget). How to fix it: this lesson's Step 5 adds cosign.key to .gitignore before any git add, exactly in the correct order — if you already committed it by mistake, the encrypted file itself isn't a usable key without the password (a real advantage of it being encrypted), but the correct practice is still to regenerate a new keypair and purge the exposed commit, not to trust the encryption as sufficient excuse to leave it in the history.
Assuming an empty password (COSIGN_PASSWORD="") is bad practice in any context, without distinguishing this lab from a real case (overgeneralization mistake). What happens: someone, seeing COSIGN_PASSWORD="" in this lesson, concludes it's the "correct" way to handle this step in any project. How to spot it: if you copy this exact command into a project with a real production artifact, without changing anything. How to fix it: an empty password is acceptable here for exactly the same reason AWS_ACCESS_KEY_ID=test is throughout this guide — nothing real is protected behind it. In a project with a real production artifact, COSIGN_PASSWORD should come from a real secrets manager (SSM Parameter Store, Secrets Manager, or GitHub Actions' secrets.* equivalent), never a hand-written empty string — the automation mechanism is the same; what changes is where the value comes from.
Exercises
Exercise 1 — Explain, without looking at this lesson, why cosign.key has 600 permissions while cosign.pub has 644. A colleague, reviewing the project, asks why the two files generated by the same command have different permissions. What would you tell them?
See solution
cosign.key holds the private key, encrypted with a password — whoever can read that file AND knows the password can sign in your name —, so restricting its reading to only the file's owner (600 permissions) is an additional defense layer, independent of the content's own encryption: even another user on the same operating system, without the password, shouldn't even be able to copy the file. cosign.pub holds the public key, whose entire purpose is for anyone to be able to use it to verify a signature — restricting its reading would add no extra security and would only get in the way of its normal use case (sharing it widely) —, so cosign generate-key-pair leaves it with open read permissions (644) by default.
Exercise 2 — Predict what would happen if you ran cosign generate-key-pair a second time, in the same directory, without deleting the existing files. Would you expect cosign to silently overwrite cosign.key/cosign.pub, or to do something different?
See solution
cosign detects that the files already exist and refuses to overwrite them by default, to prevent someone from accidentally losing an existing keypair that might be in use (for example, if lesson 6's manifest.sig was already generated with the previous key, silently overwriting it would break that signature's verification with no warning at all). The command reports an error indicating the files already exist, and an explicit flag or manually deleting the files first is needed, to confirm the intent to generate a new keypair. It's the same "never silently destroy" principle you already saw in no-destroy-shipments.rego (Module 4) — applied here to a local file, not an AWS resource.
Exercise 3 — Decide whether this lesson needed sudo at any step, and justify. Reviewing this lesson's five steps, was elevated operating-system permission needed at any point? Why or why not?
See solution
No, no step in this lesson needed sudo. Installing with brew install cosign writes inside Homebrew's directory, owned by the current user on a standard macOS install; cosign generate-key-pair writes files in the current working directory, also owned by the user. This is consistent with the least-privilege principle that has governed this entire guide since Module 2: no tool in this module needs, or should need, operating-system administrator permissions to do its job — signing a software artifact is a normal user operation, not a system operation.
Summary and next step
In this lesson you installed cosign v3.1.3, confirmed with cosign version, and generated your first local keypair with cosign generate-key-pair — resolving, with COSIGN_PASSWORD="", the real problem that this command asks for a password interactively by default, which would block any attempt to automate it. You inspected the two resulting files — cosign.key (653 bytes, 600 permissions, ENCRYPTED SIGSTORE PRIVATE KEY format) and cosign.pub (178 bytes, 644 permissions, standard PUBLIC KEY format) — and applied the same .gitignore discipline you already know from earlier modules: cosign.key never gets versioned, cosign.pub does.
Before moving on you should be able to: explain why COSIGN_PASSWORD is essential to automate this step; distinguish, by their file permissions and PEM content, which of the two generated files is safe to share; and justify why an empty password is acceptable in this specific lab, but wouldn't be in a project with a real production artifact.
With the keypair ready, lesson 6 uses cosign.key to sign lambda/function.zip for real, and cosign.pub to verify that signature — completely offline, without depending on Rekor.
Resources
- Sigstore — Signing with a self-managed key — the official documentation for
cosign generate-key-pairand password handling, including theCOSIGN_PASSWORDvariable. - GitHub — sigstore/cosign releases — the source for the
v3.1.3version confirmed in this lesson, including the detail of that version's security patch. - This course, Module 3,
secrets.tf— the same secret-management discipline (never in plain text in the repository) applied here to a cryptographic key file instead of an API credential.