Docker 56 🐳 Container Image Signing and Verification: Docker Content Trust (DCT) and Cosign
A container image tag is a mutable label. An attacker who compromises a registry can repoint myapp:latest to a malicious image, and every system that trusts the tag pulls the attacker’s code. Image signing closes this gap by attaching a cryptographic signature to the image digest. The signature proves who published the image and that it has not been modified since signing. Two tools dominate the space: Docker Content Trust (DCT), the legacy Notary v1-based system built into the Docker CLI, and Cosign, the modern Sigstore-based tool that has become the de facto standard. DCT is being retired, and Cosign is the replacement.
Key point: Signing an image is meaningless without verification. A signed image that is never verified is no more secure than an unsigned one. The signature only matters when the consumer checks it before deploying, and the check must be enforced at admission, not left to the operator’s discretion.
Why image signing exists
The mutable tag problem. A tag is a pointer, and pointers can be changed. An attacker who gains push access to a registry can overwrite the image behind a trusted tag. The consumers that pull by tag receive the malicious image without any indication that the content changed.
The supply-chain attack problem. Modern attacks target the build pipeline, not the application. A compromised CI runner can inject code into the image during the build, before the image is even pushed. Signing binds the image to the identity that produced it, which makes the injection detectable.
The provenance problem. A signature proves that an image was not modified after signing. It does not prove how the image was built. Provenance attestations, attached alongside the signature, record the build inputs: the source commit, the build system, the dependencies. The combination of signature and provenance is what makes a supply chain verifiable.
The air-gap problem. Some environments cannot reach the public Sigstore infrastructure. The trust root and the signatures must be carried across the air gap ahead of time, and verification must work offline.
The key management problem. Traditional signing requires a long-lived private key, which must be stored, rotated, and protected. A leaked key undermines every signature it produced. Keyless signing eliminates the long-lived key by using short-lived certificates bound to an OIDC identity.
The enforcement problem. A signature is only useful if the deployment platform checks it. Kubernetes admission controllers, policy engines, and CI gates are the enforcement points. Without enforcement, signing is theater.
a. Docker Content Trust (DCT)
Docker Content Trust is the legacy signing system built into the Docker CLI. It uses Notary v1, a TUF-based framework, to sign image tags and verify them at pull time.
The DCT model
DCT signs the tag, not the digest. The signature is stored in a Notary server attached to the registry. When DCT is enabled, the Docker CLI verifies the signature before pulling, running, or building with an image .
# Enable DCT for the shell session
export DOCKER_CONTENT_TRUST=1
# With DCT enabled, docker pull verifies the signature
docker pull myregistry.example.com/my-app:v1.5.0
# docker push signs the image during the push
docker push myregistry.example.com/my-app:v1.5.0
The DOCKER_CONTENT_TRUST environment variable is the switch. When set to 1, every pull is verified and every push is signed. When unset, DCT is off and every image is visible regardless of whether it is signed .
The DCT keys
Trust for an image tag is managed through five classes of keys :
| Key | Purpose | Storage |
|---|---|---|
| Root | Root of trust for the repository | Offline, hardware-backed |
| Targets | Signs image tags and manages delegations | Client-side |
| Snapshot | Signs the current collection of tags | Server-side |
| Timestamp | Provides freshness guarantees | Server-side |
| Delegation | Optional keys for delegated signing | Client-side |
The root key is the most sensitive. It is created once, should be kept offline, and can be stored on a YubiKey 4 . Losing the root key is not recoverable, and every consumer that used a signed tag from the repository requires manual intervention .
Signing with DCT
The docker trust command generates the delegation key, initializes the repository, and signs the image .
# Generate a delegation key pair
docker trust key generate my-signing-key
# Initialize signing for the repository
docker trust signer add --key my-signing-key.pub my-signer \
myregistry.example.com/my-app
# Sign and push
DOCKER_CONTENT_TRUST=1 docker push myregistry.example.com/my-app:v1.5.0
# Inspect the signatures
docker trust inspect --pretty myregistry.example.com/my-app:v1.5.0
The DCT retirement
Docker Content Trust is being retired. The Notary v1 service at notary.docker.io shuts down on December 8, 2026 . Azure Container Registry will no longer allow DCT to be enabled on new registries after May 31, 2026, and DCT will be fully removed by March 31, 2028 . The migration path is the Notary Project (Notary v2) and Cosign.
The DCT limitations
DCT has several limitations. It signs tags, not digests, which means the signature is bound to the mutable label . It requires a Notary server attached to the registry, which limits the registries that support it . It does not work with repository-scoped tokens in Azure Container Registry . And the key management is manual and error-prone.
b. Cosign and Sigstore
Cosign is the modern replacement for DCT. It is part of Sigstore, a set of open-source components that make signing practical without the key management burden.
The Sigstore components
Sigstore has three core components :
| Component | Purpose |
|---|---|
| Cosign | The CLI that signs and verifies images |
| Fulcio | The CA that issues short-lived certificates bound to OIDC identities |
| Rekor | The transparency log that records signing events |
The signature and attestations are stored alongside the image in the OCI registry, as a separate artifact under a tag derived from the image digest .
Keyless signing
Keyless signing eliminates the long-lived private key. Cosign requests a short-lived certificate from Fulcio, bound to an OIDC identity, signs with an ephemeral key, and records the event in Rekor .
# Sign the image by digest with keyless OIDC
cosign sign --yes ghcr.io/acme/api@sha256:a1b2c3d4e5f6...
In CI, the OIDC identity is the pipeline’s own token. For GitHub Actions, the id-token: write permission is required .
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: sigstore/cosign-installer@v3.5.0
- run: cosign sign --yes ${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
The signature proves “this identity signed this image at this time,” and the Rekor log proves the record cannot be altered retroactively .
Key-based signing
For air-gapped or high-assurance environments, Cosign supports key-based signing with a generated key pair .
# Generate a key pair
cosign generate-key-pair
# Sign by digest
cosign sign --key cosign.key \
registry.example.com/myapp@sha256:abc123...
# Verify against the public key
cosign verify --key cosign.pub \
registry.example.com/myapp@sha256:abc123...
Verification
Cosign verification asserts the certificate identity and the OIDC issuer .
cosign verify \
--certificate-identity-regexp "https://github.com/acme/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/api@sha256:a1b2c3d4e5f6... | jq
Verification fails closed if the transparency log entry is missing, the certificate is expired, or the identity does not match .
Attestations
Cosign can attach SBOMs and provenance attestations alongside the signature .
# Attach an SBOM
cosign attest --predicate sbom.cdx.json \
--type cyclonedx \
registry.example.com/myapp@sha256:abc123...
# Download the SBOM
cosign download attestation \
--predicate-type https://spdx.dev/Document \
registry.example.com/myapp@sha256:abc123...
The attestations are signed and stored as OCI artifacts, and a verifier can require a matching SBOM and build provenance as part of the policy .
c. Enforcement and the air gap
Signing without verification is useless. The verification must happen where the image is deployed, and it must be enforced automatically.
Admission control
The Sigstore policy-controller is a Kubernetes admission webhook that rejects unsigned or misattested pods before they run . A ClusterImagePolicy requires a signature from a specific identity.
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-acme-signature
spec:
images:
- glob: "ghcr.io/acme/**"
authorities:
- keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/acme/.*"
The webhook rejects any pod whose image does not have a matching signature.
The air-gapped verification
An air-gapped environment cannot reach the public Sigstore infrastructure. Verification requires two things: the Sigstore trust root and the signatures themselves .
# On a connected machine: download the trust root
cosign initialize
cp ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/targets/trusted_root.json .
# Copy the trust root and the image signatures into the air gap
# Then verify with the trust root as a file
cosign verify \
--trusted-root trusted_root.json \
--certificate-identity "..." \
--certificate-oidc-issuer "..." \
registry.internal/myapp@sha256:...
The trust root is about 7 KB and holds the Fulcio CAs, Rekor keys, and timestamp authority certificates . The signature is stored under a tag derived from the image digest, so copying the image alone leaves the signature behind .
The vulnerable certificate chain
A known issue in Cosign affects private deployments with customized PKIs. When transparency log verification is skipped, Cosign considers signatures valid with expired intermediate certificates if the leaf certificate’s “not before” timestamp is within the intermediate’s validity . The issue is unlikely in practice because CAs should not issue certificates that outlive the issuing CA, but private deployments should verify the certificate chain out of band or upgrade to the latest release .
Complete Example Session
# ============================================
# PART 1: DOCKER CONTENT TRUST (LEGACY)
# ============================================
# Enable DCT
export DOCKER_CONTENT_TRUST=1
# Generate a delegation key
docker trust key generate my-signing-key
# Initialize the repository
docker trust signer add --key my-signing-key.pub my-signer \
myregistry.example.com/my-app
# Sign and push
docker push myregistry.example.com/my-app:v1.5.0
# Inspect
docker trust inspect --pretty myregistry.example.com/my-app:v1.5.0
# ============================================
# PART 2: COSIGN KEYLESS SIGNING
# ============================================
# Sign by digest with keyless OIDC
cosign sign --yes ghcr.io/acme/api@sha256:a1b2c3d4e5f6...
# ============================================
# PART 3: COSIGN KEYLESS VERIFICATION
# ============================================
cosign verify \
--certificate-identity-regexp "https://github.com/acme/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/api@sha256:a1b2c3d4e5f6... | jq
# ============================================
# PART 4: COSIGN KEY-BASED SIGNING
# ============================================
cosign generate-key-pair
cosign sign --key cosign.key \
registry.example.com/myapp@sha256:abc123...
cosign verify --key cosign.pub \
registry.example.com/myapp@sha256:abc123...
# ============================================
# PART 5: COSIGN ATTESTATION
# ============================================
cosign attest --predicate sbom.cdx.json \
--type cyclonedx \
registry.example.com/myapp@sha256:abc123...
# ============================================
# PART 6: GITHUB ACTIONS SIGNING
# ============================================
# .github/workflows/release.yml
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: sigstore/cosign-installer@v3.5.0
- run: cosign sign --yes ${IMAGE}@${DIGEST}
# ============================================
# PART 7: ADMISSION POLICY
# ============================================
# ClusterImagePolicy requiring a keyless signature
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-acme-signature
spec:
images:
- glob: "ghcr.io/acme/**"
authorities:
- keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/acme/.*"
# ============================================
# PART 8: AIR-GAPPED VERIFICATION
# ============================================
cosign initialize
cp ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/targets/trusted_root.json .
# Move trusted_root.json and the image signatures into the air gap
cosign verify \
--trusted-root trusted_root.json \
--certificate-identity "..." \
--certificate-oidc-issuer "..." \
registry.internal/myapp@sha256:...
# ============================================
# PART 9: DCT TO COSIGN MIGRATION
# ============================================
# DCT is retired on December 8, 2026
# Migrate to Cosign for keyless signing
# Replace docker trust commands with cosign commands
# Update admission policies to verify Cosign signatures
# ============================================
# PART 10: THE COMPLETE PATTERN
# ============================================
# 1. Sign by digest, never by tag.
# 2. Use keyless OIDC for CI-built images.
# 3. Use key-based signing for air-gapped environments.
# 4. Attach SBOMs and provenance as attestations.
# 5. Enforce signatures at admission control.
# 6. Carry the trust root across the air gap.
# 7. Migrate from DCT to Cosign before December 2026.
The ten parts showed DCT signing, Cosign keyless signing, Cosign verification, key-based signing, attestations, GitHub Actions integration, admission policy, air-gapped verification, the DCT migration, and the complete pattern.
Quick Reference
DCT vs Cosign
| Aspect | DCT | Cosign |
|---|---|---|
| Signing target | Tag | Digest |
| Framework | Notary v1 (TUF) | Sigstore |
| Key management | Manual, 5 key types | Keyless or key-based |
| Transparency log | Notary server | Rekor |
| Storage | Notary server | OCI registry |
| Status | Retired Dec 2026 | Active |
| Air-gap support | Limited | Supported with trust root |
Cosign Commands
| Command | Purpose |
|---|---|
cosign sign | Sign an image |
cosign verify | Verify a signature |
cosign attest | Attach an attestation |
cosign download attestation | Download an attestation |
cosign generate-key-pair | Generate a key pair |
cosign initialize | Download the trust root |
cosign tree | List attached artifacts |
DCT Commands
| Command | Purpose |
|---|---|
docker trust key generate | Generate a delegation key |
docker trust signer add | Add a signer |
docker trust inspect | Inspect signatures |
DOCKER_CONTENT_TRUST=1 docker push | Sign and push |
Key Types (DCT)
| Key | Purpose |
|---|---|
| Root | Root of trust; keep offline |
| Targets | Sign tags; client-side |
| Snapshot | Sign the tag collection |
| Timestamp | Freshness guarantees |
| Delegation | Optional delegated signing |
Verification Flags
| Flag | Purpose |
|---|---|
--certificate-identity | Exact identity match |
--certificate-identity-regexp | Regex identity match |
--certificate-oidc-issuer | OIDC issuer |
--key | Public key for key-based verification |
--trusted-root | Trust root file for air-gapped |
--insecure-ignore-tlog | Skip transparency log (not recommended) |
Best Practices
✅ Do This:
# Sign by digest, never by tag.
cosign sign --yes ghcr.io/acme/api@sha256:abc... # ✅
# Use keyless OIDC in CI.
# id-token: write permission in GitHub Actions. # ✅
# Verify with identity and issuer.
cosign verify --certificate-identity-regexp "..." --certificate-oidc-issuer "..." # ✅
# Enforce at admission control.
# ClusterImagePolicy with keyless authorities. # ✅
# Carry the trust root into the air gap.
cosign initialize; cp trusted_root.json . # ✅
# Migrate from DCT to Cosign.
# DCT is retired on December 8, 2026. # ✅
# Attach SBOMs and provenance.
cosign attest --predicate sbom.cdx.json # ✅
❌ Don’t Do This:
# Don't sign by tag.
cosign sign ghcr.io/acme/api:latest # ❌ Tag is mutable
# Don't skip verification.
# A signature that is never checked is theater. # ❌
# Don't use --insecure-ignore-tlog.
# It discards the transparency log check. # ❌
# Don't enable DCT on new registries.
# DCT is retired. # ❌
# Don't forget the trust root in the air gap.
# Verification fails without it. # ❌
# Don't copy the image alone into the air gap.
# The signature is a separate artifact. # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Signature not found | Image copied without the signature | Copy the signature artifact |
| Verification fails | Wrong identity or issuer | Check the OIDC identity |
| Air-gap fails | No trust root | Pass --trusted-root |
| DCT pull fails | Certificate expired | Disable DCT; migrate to Cosign |
| CI workflow hangs | Missing id-token: write | Add the permission |
| Policy rejects unsigned | No signature | Sign before deploying |
| Tlog entry missing | Signature not logged | Re-sign with tlog enabled |
Real-World Examples
1. DCT Signing
docker trust key generate my-key
docker trust signer add --key my-key.pub my-signer myregistry/my-app
DOCKER_CONTENT_TRUST=1 docker push myregistry/my-app:v1
2. Cosign Keyless Signing
cosign sign --yes ghcr.io/acme/api@sha256:abc...
3. Cosign Verification
cosign verify --certificate-identity-regexp "https://github.com/acme/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/api@sha256:abc...
4. Key-Based Signing
cosign generate-key-pair
cosign sign --key cosign.key myregistry/myapp@sha256:abc...
5. Attestation
cosign attest --predicate sbom.cdx.json --type cyclonedx myregistry/myapp@sha256:abc...
6. GitHub Actions
- uses: sigstore/cosign-installer@v3.5.0
- run: cosign sign --yes ${IMAGE}@${DIGEST}
7. Admission Policy
kind: ClusterImagePolicy
spec:
authorities:
- keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
8. Air-Gapped Trust Root
cosign initialize
cp ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/targets/trusted_root.json .
9. DCT Inspect
docker trust inspect --pretty myregistry/my-app:v1
10. Migrate from DCT
# Replace docker trust with cosign
# Update admission policies
Visual
The Signing Flow
┌────────────────────────────────────────────────────────────┐
│ COSIGN SIGNING FLOW │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CI RUNNER │ │
│ │ - OIDC token from the pipeline │ │
│ │ - Builds the image │ │
│ │ - Pushes by digest │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ cosign sign │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ FULCIO (Certificate Authority) │ │
│ │ - Verifies the OIDC identity │ │
│ │ - Issues a short-lived certificate │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ Certificate │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ REKOR (Transparency Log) │ │
│ │ - Records the signing event │ │
│ │ - Returns a signed entry timestamp │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ Signature + tlog entry │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ OCI REGISTRY │ │
│ │ - Stores the image by digest │ │
│ │ - Stores the signature as a separate artifact │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Verification Flow
┌────────────────────────────────────────────────────────────┐
│ COSIGN VERIFICATION │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ VERIFIER │ │
│ │ - Knows the expected identity and issuer │ │
│ │ - Fetches the image by digest │ │
│ │ - Fetches the signature from the registry │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CHECKS PERFORMED │ │
│ │ 1. Signature matches the image digest │ │
│ │ 2. Certificate identity matches the expected │ │
│ │ 3. Certificate issuer matches the expected │ │
│ │ 4. Certificate is within its validity window │ │
│ │ 5. Rekor tlog entry exists and matches │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ v v │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ALL PASS │ │ ANY FAIL │ │
│ │ Deploy the │ │ Reject the │ │
│ │ image │ │ image │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Verification fails closed. │
└────────────────────────────────────────────────────────────┘
The DCT Key Hierarchy
┌────────────────────────────────────────────────────────────┐
│ DCT KEY HIERARCHY │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ROOT KEY (offline) │ │
│ │ - The root of trust for the repository │ │
│ │ - Stored offline or on a YubiKey │ │
│ │ - Loss is not recoverable │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ signs │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ TARGETS KEY (repository) │ │
│ │ - Signs the image tags │ │
│ │ - Manages the delegations │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ delegates │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DELEGATION KEY (optional) │ │
│ │ - Signs a subset of tags │ │
│ │ - Without sharing the targets key │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The snapshot and timestamp keys are server-side. │
└────────────────────────────────────────────────────────────┘
The Air-Gap Verification
┌────────────────────────────────────────────────────────────┐
│ AIR-GAP VERIFICATION │
│ │
│ CONNECTED MACHINE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ cosign initialize │ │
│ │ → downloads the Sigstore trust root │ │
│ │ │ │
│ │ cp trusted_root.json . │ │
│ │ → carries the trust root across the air gap │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ USB / approved transfer │
│ v │
│ AIR-GAPPED MACHINE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ cosign verify │ │
│ │ --trusted-root trusted_root.json │ │
│ │ --certificate-identity "..." │ │
│ │ --certificate-oidc-issuer "..." │ │
│ │ │ │
│ │ The trust root is a file, not a network fetch. │ │
│ │ The tlog entry is in the signature bundle. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| DCT | Legacy Notary v1-based signing |
| DCT status | Retired December 8, 2026 |
| Cosign | Modern Sigstore-based signing |
| Fulcio | Certificate authority for keyless signing |
| Rekor | Transparency log for signing events |
| Keyless signing | Short-lived certificate bound to OIDC |
| Key-based signing | For air-gapped and high-assurance |
| Signature storage | OCI registry, alongside the image |
| Attestation | Signed SBOM or provenance |
| Admission control | policy-controller enforcement |
Key takeaways:
- Docker Content Trust is being retired. The Notary v1 service shuts down on December 8, 2026. Azure Container Registry stops allowing DCT on new registries after May 31, 2026. Cosign is the replacement, and migration should be planned now .
- Cosign signs by digest, not by tag. A tag is mutable and a signature bound to a tag is worthless the moment the tag is repointed. Every
cosign signcall resolves to a digest before signing, and every verification should be against a digest-pinned reference . - Keyless signing eliminates the long-lived key. Cosign requests a short-lived certificate from Fulcio, bound to an OIDC identity, and records the event in Rekor. In CI, the identity is the pipeline’s own OIDC token, which proves which workflow produced the image .
- Verification asserts the identity and issuer. The
--certificate-identityand--certificate-oidc-issuerflags are the verification contract. A signature from the wrong identity or issuer is rejected . - Attestations bind the image to its provenance. SBOMs and build provenance are attached as signed OCI artifacts. A verifier can require a matching SBOM and provenance as part of the admission policy .
- Enforcement at admission is what makes signing real. The Sigstore
policy-controllerrejects unsigned or misattested pods. Signing without enforcement is theater . - Air-gapped verification requires the trust root. The Sigstore trust root is carried across the air gap as a file, and the signatures are copied alongside the image. The transparency log entry is in the signature bundle, so verification works offline .
- A known Cosign issue affects private PKIs. When transparency log verification is skipped, an expired intermediate certificate may be considered valid. Private deployments should verify the chain out of band or upgrade .
Remember: Container image signing closes the supply-chain gap that tags leave open. Docker Content Trust is the legacy system, built on Notary v1, and it is being retired. Cosign is the modern replacement, built on Sigstore, and it has become the de facto standard. Keyless signing binds the signature to an OIDC identity without a long-lived key, and key-based signing covers the air-gapped and high-assurance cases. The signature is stored alongside the image in the OCI registry, and attestations bind the image to its provenance. Verification must be enforced at admission control, or the signature is meaningless. The migration from DCT to Cosign should be planned before December 2026, when the Notary v1 service shuts down.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!