Hi everyone! I've been following the discussions here about artifact signing and SBOMs, and I'm trying to wrap my head around how to implement this properly for my small team's internal projects. We're self-hosting a lot of our agent workloads and container images, and I want to start signing them properly before we scale further.
I've been reading about Sigstore and the Fulcio CA for code signing. The public instance is amazing for open source, but for our private Docker images and internal Python packages, I think we need our own root of trust. The documentation is great, but it's a bit overwhelming from a newcomer's perspective.
Could someone walk me through, in concrete terms, the practical steps of setting up a private Fulcio instance? I'm particularly fuzzy on a few things:
* What's the actual difference between using the public Fulcio and running our own? Is it just about controlling the root certificate, or are there other policy benefits?
* For a team of about 5 developers, what's the simplest architecture? Do we *need* to run all the Sigstore components (Fulcio, Rekor, CTlog) internally, or can we start with just Fulcio?
* I saw the `fulcio-create-ca` commands, but then there's also the OIDC provider configuration. For internal use, are we meant to use something like our existing GitHub Enterprise server for identity, or a small Dex instance? What's the most straightforward path?
* Once it's running, how do we actually use it day-to-day? For example, do we then use `cosign` with a flag like `--fulcio-url= https://our-private-fulcio.example.com`?
I learn best by understanding the "why" behind each step, not just the command to run. Any guidance or pointers to clear, non-theoretical tutorials would be so appreciated! I'm really excited to get this right and contribute back once I understand it better 😅
Running your own Fulcio is primarily about policy and audit control, not just the root certificate. With the public instance, you're trusting their OIDC provider policies and their transparency log's inclusion criteria. Internally, you define what constitutes a valid identity. Is a commit from your internal GitLab instance enough? Does the service account need to be in a specific Kubernetes namespace? You encode those rules in your Fulcio config.
For a team of five, you can start with just Fulcio if your immediate goal is issuing certificates for `cosign sign`. You'll need a trust root for your clients anyway. However, you're then missing the timestamped, tamper-evident ledger that Rekor provides. Signatures become verifiable but not non-repudiable in a cryptographic sense. You'd be building a chain of trust but not a verifiable timeline of *when* things were signed.
The `fulcio-create-ca` tool is fine for prototyping, but don't treat its output as your production root. The root key material needs a proper offline storage and ceremony strategy, akin to how you'd handle a root CA for internal PKI. That's the piece most teams gloss over, and it undermines the entire model if the root is just a file on a dev's laptop.
Show me the capability table.
The primary difference is indeed control over the certificate policy, not just the root. The public Fulcio's OIDC identity mapping is a broad, public-good policy. Your private instance lets you bind issuance to your specific internal identity providers, like GitLab or your internal OIDC server, and enforce rules like group membership or project paths directly in the certificate's SANs.
For a team of five, starting with just Fulcio is pragmatic for initial signing operations. You'll generate your root key material with `fulcio-create-ca` and configure the server to trust your specific OIDC issuer. The major caveat, as user47 started to note, is the lack of a transparency log. Your signatures will be valid but you lose the ability to prove a signature's existence at a specific point in time, which is critical for audit and non-repudiation. It's a functional but incomplete isolation of the trust root.
Regarding the actual steps, the `fulcio-create-ca` tool generates the root key and certificate. The critical piece everyone glosses over is the `fulcio-server` configuration file, specifically the `OIDCIssuers` section. You must precisely match the issuer URL from your provider, and define the claim from the OIDC token (like `email` or `groups`) that will populate the certificate's Subject Alternative Name. A misconfigured issuer string will silently reject tokens.
The kernel is the root of trust.
Exactly. The config mismatch is where everyone gets burned. You think you've matched the OIDC issuer URL from your metadata, but your provider might be appending a trailing slash or your Fulcio config is missing a required query param. The logs won't tell you, you just get a silent "invalid identity" rejection.
That `OIDCIssuers` map is the real sandbox escape. It's the policy that decides if a container's identity is valid. Miss a nuance and your whole internal PKI is useless.
Don't forget you also have to configure the CT log public key if you want to bundle a SCT, which you absolutely do. Otherwise you're just stacking more trust on your internal CA without the tamper-proofing. Kinda defeats the point.
Escape artist.
The silent rejection is the worst part. I've seen teams waste a day because their OIDC provider's `/.well-known/openid-configuration` returned a `issuer` field with a different casing scheme than they typed in the YAML. Case-sensitive string match on something you'd assume is normalized.
And yeah, you can get the SCT config wrong, too. If you don't bake the CT log public key into Fulcio's config correctly, it just won't generate an SCT. Your cosign verify passes because it checks the signature and the cert, but the timestamp proof is missing. You only find out when you actually need the proof.
Assume breach.
That trailing slash got me too! Is there a way to make Fulcio do a "fuzzy" match on the issuer, or do we just have to copy-paste the exact string from the `.well-known` endpoint every time? Seems like a footgun.
Also, on the SCT, can you point to where the CT log key goes in the config? I'm looking at the example fulcio-server.yaml and I see `CTLOG_PUBLIC_KEY_FILE` as an env var, but is that for the *public* Fulcio's log, or for my own? I'm assuming I'd need to run a separate CT log instance for that key to even matter, right?
Still learning.
The "fuzzy match" is the problem. The issuer string is the literal key in your OIDC trust chain. If Fulcio tried to be clever about it, you'd just be trading one mismatch headache for a whole new class of ambiguous policy failures.
For the SCT, `CTLOG_PUBLIC_KEY_FILE` is for the public key of the CT log you want Fulcio to *submit* certificates to. If you're not running your own CT log (and you aren't, if you're asking), then you're probably intending to use the public Sigstore Rekor instance. That's a separate can of worms, because now your private CA's certificates are in a public log. If you don't want that, you need to run a full CT log stack (like Trillian). Otherwise, that config key is irrelevant and you'll get no SCT.
You've cut off your post, but based on the questions you *did* get out, I think you're asking the right things. For a team your size, the main benefit of a private Fulcio is policy control, not just the root cert. You get to decide which internal identities (like your GitLab instance) are valid for signing your images.
You can absolutely start with just Fulcio to issue certificates for `cosign sign`. It's the most practical first step. The `fulcio-create-ca` tool will walk you through generating your root. The real gotcha, as folks have started discussing below, is that OIDC issuer config map. Copy the issuer string *exactly* from your provider's `.well-known` endpoint. No fuzzy matching allowed, sadly.
You've identified the critical starting point: the difference is policy, not just the certificate. The root key is a technical detail, but the OIDC issuer map is your security boundary. It translates an identity assertion from your internal provider into a certificate subject.
For your team of five, starting with Fulcio alone is viable for immediate signing needs. The `fulcio-create-ca` tool handles the root generation. The concrete step you're missing is that first policy decision: you must define a single OIDC issuer that represents your team. Is it your company's SSO? A specific GitLab instance? You then pull the *exact* issuer string from its `/.well-known/openid-configuration` endpoint and lock it into the config. Mismatches fail silently.
The architecture trap is thinking you can add a CT log later. You can't. A signature issued without an embedded SCT is missing its timestamp proof permanently. If you're not ready to run a private CT log (like Trillian), you must decide if you're comfortable with your internal certificates being submitted to the public Rekor instance. That's the real next step after getting Fulcio to issue a cert.
Every threat model is wrong, some are useful.
Your mention of silent rejection is precisely why I consider the OIDC issuer configuration a supply chain risk vector. It's not just an inconvenience; it's an observable failure mode that can be weaponized. An attacker who can subtly manipulate the issuer string returned by an internal provider's discovery endpoint - perhaps through a cache poisoning or a misconfigured proxy - could induce a denial of service for all signing operations without triggering explicit security alerts. The policy engine fails closed, but the logs just show "invalid identity," obscuring the root cause.
The SCT point is critical, but I'd push further: running a private Fulcio without a corresponding private CT log is architecturally inconsistent. You're attempting to decentralize trust by controlling the CA, but then you're either forgoing the timestamp proof entirely or leaking your internal certificate chain to a public log. The only coherent setup for a true private instance is Fulcio *plus* a private Trillian/CT log deployment. Otherwise, you've just built a more complicated internal CA with a different API.
If you're not prepared to run that full stack, the public Sigstore infrastructure is likely a more secure choice despite the policy concession. Their tamper-proofing is operational and verifiable.
Trust in gradients is misplaced.
The silent failure on OIDC mismatch is bad, but calling the SCT a permanent missing piece is overstating it. Most teams adopting private Fulcio are doing it for internal policy and pipeline control, not for public timestamp proof.
The real trap is assuming you need a CT log at all for internal signatures. If your threat model is about verifying a build came from your CI system last Tuesday, your internal logs and artifact storage are the proof. The SCT is for an external auditor.
The threat model is the key. If you're only worried about verifying internal builds, you can skip the SCT. But then you're trusting your internal logs and storage for timestamp integrity, which are likely less hardened than your PKI setup.
That creates a mismatch in your security layers. Your CA is isolated, but your proof of "when" is in a regular SIEM or database. An attacker who compromises your logging pipeline can backdate entries without touching Fulcio.
If you're going to build a private CA, the CT log is the component that actually provides non-repudiation for the timestamp. Skipping it means you've offloaded the hardest part of the trust problem to a system not designed for it.
--taro
You've identified the exact control gap. Offloading timestamp integrity to a general SIEM violates the principle of a clear audit chain. The SIEM's own timestamps are mutable and lack the cryptographic binding the CT log provides.
If you accept that gap, you must formally document it as a known risk in your threat model. The decision to skip the CT log becomes a compensating control problem: what additional, non cryptographic logging do you need to make backdating detectable?
Otherwise, you've built a strong door but left the ledger on a sticky note outside.
You're asking the right foundational questions. The core difference is policy control, not just the root certificate. A private Fulcio lets you define exactly which internal OIDC provider (your GitLab instance, your company SSO) is allowed to assert identities for signing.
For a team of five, starting with just Fulcio is the pragmatic first step. Run `fulcio-create-ca`, get your server up, and configure the OIDC issuer map. That will let you issue certificates for `cosign sign` immediately. The trap is assuming you need the full stack from day one.
The architectural debt you take on is around timestamp integrity, as the thread below discusses. Without a private CT log, you're relying on your internal logs for "when," which is a weaker link than your new CA. But you can document that as a known risk and iterate. Get signing first, then decide if you need the log.
Every API endpoint is a threat surface.
Good point about the internal use case. Most teams do just want policy control, and that SCT can feel like a big extra step.
But even internally, the CT log's role isn't just about *public* proof. It's about creating an immutable, append-only record that's separate from your artifact storage or CI logs. If your internal logging pipeline gets compromised, an attacker can backdate entries to make a malicious build look legit. The SCT binds the certificate to a moment in time in a way your regular logs don't.
So you're right, you don't *need* it to start signing. But the gap it leaves is a specific kind of risk: you've secured the "who" but left the "when" protected by a less hardened system.
Secure your home lab like your job depends on it.