How Cal.com Rebuilt AppSec After Going Closed Source
How Cal.com consolidated noisy security tooling into one continuous, context-aware pull request security program with Gecko.
Gecko Security

You switched to GHCR because storage is free and you’re tired of Docker Hub’s pull limits breaking your builds. Then you spend an afternoon figuring out why docker push fails with permission denied even though you authenticated successfully, why your organization packages need SSO authorization on tokens, and which combination of read:packages, write:packages, and repo scopes you actually need. The authentication system connects to GitHub’s existing permissions in ways that create non-obvious failure modes.
TLDR:
GHCR (GitHub Container Registry) is GitHub’s native container image storage service, accessible at ghcr.io. If you’re already using GitHub for source control, GHCR lets you store and distribute OCI-compliant container images in the same ecosystem where your code lives.
GHCR replaced the older Docker package support that lived under docker.pkg.github.com. The registry treats container images as first-class artifacts tied directly to repositories or organizations, not generic packages. You get better visibility, access controls inherited from your repository settings, and tighter coupling with GitHub Actions workflows.
When you push an image to ghcr.io, it’s stored as an OCI artifact that any Docker-compatible client can pull. The registry follows OCI specifications, so you can use standard Docker commands or tools like Podman. Images can be public or private, scoped to your user account or organization, and linked to specific repositories for automatic visibility and permission inheritance.
GHCR currently operates under a different billing model than other GitHub Packages offerings. Container image storage and bandwidth are free, making it one of the more accessible registry options if you’re already in the GitHub ecosystem.
This creates confusion because GitHub Packages (for npm, Maven, NuGet, and RubyGems) follows a tiered billing structure with included storage and transfer limits. GHCR doesn’t fall under those same billing rules. According to GitHub’s official documentation, the Container Registry remains free for now.
GitHub has committed to providing at least one month advance notice before introducing any pricing changes. If you’re considering GHCR for production workloads, this gives you time to react, but it’s worth factoring into long-term infrastructure planning.
One detail that matters for CI/CD: bandwidth used when pulling or pushing images within GitHub Actions workflows stays free regardless of future billing changes. Your automated pipelines won’t rack up transfer costs even if external bandwidth gets metered later.
Docker Hub remains the default registry for many teams, but GHCR changes the calculation if you’re already working in GitHub’s ecosystem. The core difference comes down to rate limits and integration friction.
Docker Hub’s free tier restricts you to 200 container pulls every six hours for anonymous users, which breaks CI/CD pipelines fast. GHCR gives you unlimited pulls for public images and no artificial throttling on private repositories tied to your GitHub account.
Access control works differently too. Docker Hub manages permissions separately from your source code, so you’re maintaining two sets of rules. GHCR inherits repository permissions directly from GitHub, meaning anyone who can read your repo can pull images scoped to it.
For GitHub Actions workflows, GHCR authentication happens automatically through the built-in GITHUB_TOKEN. Docker Hub requires separate credential management and secret storage.
| Feature | GHCR (GitHub Container Registry) | Docker Hub |
|---|---|---|
| Rate Limits (Public Images) | No rate limits on public or private image pulls | 200 pulls per 6 hours for anonymous users; 200 pulls per 6 hours for free authenticated accounts |
| Storage & Bandwidth Pricing | Currently free with no storage or bandwidth charges; GitHub committed to 1-month advance notice before any pricing changes | Free tier includes unlimited public repositories; private repositories limited by plan tier |
| Authentication in GitHub Actions | Automatic authentication via built-in GITHUB_TOKEN; no separate credential management needed | Requires manual secret storage and separate credential management for workflow authentication |
| Access Control Model | Inherits permissions directly from GitHub repository settings; unified access management with source code | Separate permission system independent of source code repositories; requires dual maintenance |
| Image Namespace Structure | ghcr.io/NAMESPACE/IMAGE where namespace matches GitHub username or organization | docker.io/NAMESPACE/IMAGE with separate Docker Hub account namespace |
| Public Image Discovery | No centralized browse interface; discovery through GitHub package search or repository links | Centralized search and browse interface with filtering by categories and popularity |
| CI/CD Pipeline Impact | Unlimited pulls prevent pipeline throttling; tight GitHub Actions integration reduces configuration overhead | Rate limits frequently break CI/CD pipelines on free tier; requires paid plans or pull-through caching strategies |
Authentication to ghcr.io relies on GitHub’s native credential system. Local development requires a Personal Access Token with appropriate package scopes, while GitHub Actions workflows use the automatically provided GITHUB_TOKEN.
Generate a PAT from Developer Settings > Personal Access Tokens. Reading public and private images requires the read:packages scope. Pushing containers needs write:packages. When images tie to private repositories, add the repo scope for proper permission inheritance.
Authenticate Docker or Podman with your token:
<code class="language-css">echo YOUR_PAT | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
</code>The --password-stdin flag prevents tokens from appearing in shell history. Podman uses identical syntax.
For GitHub Actions, skip manual PAT creation. The GITHUB_TOKEN secret handles GHCR authentication automatically:
<code class="language-yaml">- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
</code>This workflow-scoped token expires when the job completes, limiting credential exposure.
Image names on GHCR follow the structure ghcr.io/NAMESPACE/IMAGE_NAME:TAG, where the namespace matches your GitHub username or organization. Tag and push your local image:
<code class="language-bash">docker tag myapp:latest ghcr.io/youruser/myapp:v1.0.0
docker push ghcr.io/youruser/myapp:v1.0.0
</code>Pulling public images requires no authentication:
<code class="language-python">docker pull ghcr.io/open-webui/open-webui:main
</code>Private images need your PAT or GITHUB_TOKEN first. GHCR supports multi-architecture manifests via Docker buildx:
<code class="language-bash">docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/youruser/myapp:latest --push .
</code>Image visibility defaults to private. You can switch to public in package settings without exposing your repository if it’s private, though testing for hidden attack vectors remains important.
GitHub Actions workflows authenticate to GHCR using the built-in GITHUB_TOKEN, which eliminates separate credential management. The workflow context injects repository names and commit metadata for automatic tagging.
A typical workflow grants package write permissions and logs in before building:
<code class="language-yaml">name: Build and Push to GHCR
on:
push:
branches: [main]
<p>jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
</code>The permissions block is required for push operations. Omitting it causes authentication failures despite valid tokens. For deployments spanning multiple registries like Docker Hub or AWS ECR, store additional credentials as repository secrets and execute sequential login steps before tagging images with registry-specific prefixes.
Container registries store more than just images. They inherit every vulnerability, hardcoded secret, and misconfiguration baked into the layers you push. 87% of container images contain high or critical security vulnerabilities, which creates immediate exposure when pulling from GHCR without scanning.
Base image vulnerabilities compound across your stack. An outdated Alpine or Ubuntu layer propagates the same CVEs to every dependent image. Exposed secrets like API keys or database credentials embedded in layers remain accessible even after deletion, since registry layers are immutable and cached across pulls.
Supply chain attacks target registries directly. Typosquatting, compromised upstream images, and malicious tags inserted into public namespaces turn trusted sources into infection vectors. GHCR’s public image discoverability increases attack surface if you’re pulling third-party containers without verification.
Registry scanning catches known CVEs but misses business logic flaws in the applications running inside containers.
GHCR doesn’t offer a centralized search interface like Docker Hub’s browse page. You can’t land on ghcr.io and browse popular images or filter by categories. Discovery happens through GitHub’s package search or direct repository links.
Search for public GHCR images from GitHub’s main search bar using the type:package filter. Add registry constraints like ghcr.io open-webui to narrow results. Repository README files often include ghcr.io pull commands, making the source repo your best discovery path.
Package visibility settings live in repository or organization package settings, not at push time. Switching an image from private to public doesn’t expose the linked repository if it remains private. You control package and code visibility independently.
Verify authenticity by checking the package’s linked repository and organization. GHCR displays the source repository for each image, letting you confirm the maintainer matches expectations before pulling, similar to how authorization bypass vulnerabilities require verification of authentication flows.
Authentication failures usually trace back to token scope. If docker login ghcr.io succeeds but pushes fail with “denied: permission_denied”, your PAT is missing write:packages. Pulling private images linked to private repos needs the repo scope added.
Organization accounts with SSO require token authorization. After generating your PAT, click “Configure SSO” next to the token and authorize it for your organization. Without this step, you’ll authenticate successfully but hit permission errors on org-scoped packages.
Permission denied errors during push often mean the package namespace doesn’t match your authentication context. Pushing to ghcr.io/orgname/image requires membership in orgname with package write access. Verify the namespace matches your username or an organization you belong to.
GHCR doesn’t impose Docker Hub-style rate limits on public image pulls, but GitHub Actions workflow failures can stem from insufficient job permissions. Add packages: write to the job’s permissions block when pushing images.
Registry scanning tools catch CVEs in base images and dependencies, but they can’t analyze what happens when your containerized application runs. A clean scan on your GHCR image tells you nothing about authorization bypasses, privilege escalation paths, or broken access control in the application code inside that container.
Container scanning operates at the dependency layer, checking Alpine versions, library CVEs, and known vulnerabilities in packages. Static analysis struggles with business logic vulnerabilities in your application code. Business logic flaws live in your application code. They come from how services interact across trust boundaries, how authentication flows between microservices, and whether permission checks actually exist where they should.
Gecko finds semantic vulnerabilities that scanning misses by analyzing how containerized applications actually behave and tracing execution paths across service boundaries to identify authorization gaps that only appear at runtime.
GHCR solves the Docker Hub rate limit problem and integrates cleanly with your GitHub workflows, but container security needs more than vulnerability scanning. Your business logic flaws live in how services trust each other and handle permissions, not in outdated base images. Gecko traces those execution paths across your containerized stack to catch semantic vulnerabilities that registry tools miss.

Artemiy Malyshau
Co-founder & CTO
Artemiy served in an elite unit of the Austrian Cyber Forces, defending national infrastructure He was then the first employee at a government-backed cybersecurity research group, where he led security projects for Interpol and national governments. At Gecko he builds the platform trusted to sit inside Fortune 500 codebases, and holds it to the standard those governments taught him.
The latest news, technologies, and resources from our team.
How Cal.com consolidated noisy security tooling into one continuous, context-aware pull request security program with Gecko.
Gecko Security
Authorization bypass in n8n’s dynamic-credentials OAuth endpoints allows any authenticated user to operate on another user’s OAuth credential by supplying its ID, enabling unauthorized OAuth rebinding and revocation.
Artemiy Malyshau
An IDOR vulnerability in n8n’s public variables API allows authenticated users to read project variables outside their authorized scope, exposing secrets across project boundaries.
Artemiy Malyshau
Learn API scanning for automated security testing. Find vulnerabilities from broken authentication to business logic flaws in your endpoints.
Artemiy Malyshau
A complete guide to automated pentest tools and best practices. Learn what works, what doesn’t, and how to implement continuous security testing.
Artemiy Malyshau
Compare the best AI-powered application security testing tools. Find which tools detect business logic flaws and broken access control.
Artemiy Malyshau
Occasional updates, new content, and insights. No spam; unsubscribe anytime.