πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Security intermediate ⏱ 15 minutes K8s 1.28+

Quay Default Permissions for Robot Accounts

Configure organizational default permissions in Quay Registry to automatically grant read access to robot accounts on every new repository. LDAP and team patterns.

By Luca Berton β€’ β€’ πŸ“– 6 min read

πŸ’‘ Quick Answer: In Quay, go to Organization β†’ Settings β†’ Default Permissions β†’ Create Default Permission. Select the robot account (e.g., orgname+ci_pull), set permission to Read, and choose β€œAnyone” as the creator scope. Every new repository created in the organization will automatically grant the robot read (pull) access β€” no manual per-repo configuration needed.

The Problem

In large organizations with hundreds of Quay repositories, manually granting pull access to robot accounts on each new repository is:

  • Error-prone β€” developers forget to add the CI/CD robot account
  • Time-consuming β€” each repo needs individual permission configuration
  • Inconsistent β€” some repos accessible, others return 401 Unauthorized
  • Hard to audit β€” no single place to verify which repos the robot can access

Kubernetes ImagePullBackOff errors appear when a new repository is created but the robot account used in pull secrets hasn’t been granted access yet.

The Solution

Step 1: Create the Robot Account

# Via Quay API β€” create organization-level robot account
curl -X PUT "https://quay.example.com/api/v1/organization/myorg/robots/ci_pull" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"description": "CI/CD pull-only robot for Kubernetes clusters"}'

# Response includes the robot token
# {
#   "name": "myorg+ci_pull",
#   "token": "ABC123...",
#   "description": "CI/CD pull-only robot for Kubernetes clusters"
# }

Step 2: Configure Default Permissions (UI)

  1. Navigate to Organization β†’ Settings β†’ Default Permissions
  2. Click Create Default Permission
  3. Configure:
    • Permission: Read (pull only)
    • Applied to: Select robot account myorg+ci_pull
    • Creator: Anyone (applies regardless of who creates the repo)
  4. Click Create Default Permission

Step 3: Configure Default Permissions (API)

# Create default permission via API
# Grant read access to robot 'ci_pull' on all new repos
curl -X POST "https://quay.example.com/api/v1/organization/myorg/prototypes" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "read",
    "delegate": {
      "kind": "user",
      "name": "myorg+ci_pull"
    }
  }'

# Verify default permissions are set
curl -s "https://quay.example.com/api/v1/organization/myorg/prototypes" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" | jq '.prototypes[] | {
    role: .role,
    delegate: .delegate.name,
    activating_user: .activating_user.name
  }'

Step 4: Apply to Existing Repositories

Default permissions only affect new repositories. For existing repos, batch-apply:

#!/bin/bash
# grant-robot-read-all-repos.sh
# Grants read access to robot account on all existing repos

ORG="myorg"
ROBOT="myorg+ci_pull"
QUAY_URL="https://quay.example.com"

# List all repositories in the organization
REPOS=$(curl -s "${QUAY_URL}/api/v1/repository?namespace=${ORG}" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" | \
  jq -r '.repositories[].name')

for REPO in $REPOS; do
  echo -n "Granting read on ${ORG}/${REPO}... "
  
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    -X PUT "${QUAY_URL}/api/v1/repository/${ORG}/${REPO}/permissions/user/${ROBOT}" \
    -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{"role": "read"}')
  
  if [ "$HTTP_CODE" = "200" ]; then
    echo "βœ…"
  else
    echo "❌ (HTTP $HTTP_CODE)"
  fi
done

Step 5: Use in Kubernetes

# Create pull secret from robot token
kubectl create secret docker-registry quay-pull-secret \
  --docker-server=quay.example.com \
  --docker-username="myorg+ci_pull" \
  --docker-password="${ROBOT_TOKEN}" \
  --namespace=production

# Reference in Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  template:
    spec:
      imagePullSecrets:
        - name: quay-pull-secret
      containers:
        - name: app
          image: quay.example.com/myorg/myapp:v1.2.3

Multiple Robot Accounts Pattern

# Create role-specific robots with different default permissions

# Read-only robot for production clusters
curl -X PUT "${QUAY_URL}/api/v1/organization/myorg/robots/prod_pull" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"description": "Production clusters β€” pull only"}'

curl -X POST "${QUAY_URL}/api/v1/organization/myorg/prototypes" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"role": "read", "delegate": {"kind": "user", "name": "myorg+prod_pull"}}'

# Write robot for CI/CD pipelines (push + pull)
curl -X PUT "${QUAY_URL}/api/v1/organization/myorg/robots/ci_push" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"description": "CI/CD pipelines β€” push and pull"}'

curl -X POST "${QUAY_URL}/api/v1/organization/myorg/prototypes" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"role": "write", "delegate": {"kind": "user", "name": "myorg+ci_push"}}'

Scoped Default Permissions by Creator

# Only apply when a specific team/user creates repos
# Useful for team-specific robots

# When anyone on the "platform" team creates a repo,
# grant read to the platform robot
curl -X POST "${QUAY_URL}/api/v1/organization/myorg/prototypes" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "read",
    "delegate": {
      "kind": "user",
      "name": "myorg+platform_pull"
    },
    "activating_user": {
      "name": "platform_admin"
    }
  }'
graph TD
    A[New Repository Created] --> B{Default Permissions Configured?}
    B -->|No| C[Manual per-repo grants needed]
    B -->|Yes| D[Auto-apply permissions]
    D --> E[Robot: ci_pull β†’ Read]
    D --> F[Robot: ci_push β†’ Write]
    D --> G[Team: devops β†’ Admin]
    E --> H[K8s clusters can pull immediately]
    F --> I[CI pipelines can push immediately]
    C --> J[❌ ImagePullBackOff until fixed]
    
    style H fill:#d1fae5,stroke:#10b981
    style I fill:#d1fae5,stroke:#10b981
    style J fill:#fee2e2,stroke:#ef4444

Common Issues

Default Permissions Not Applied to Existing Repos

# Default permissions only affect NEW repositories
# Use the batch script above for existing repos

# Verify a specific repo's permissions
curl -s "${QUAY_URL}/api/v1/repository/myorg/myapp/permissions/user/" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" | jq '.permissions'

Robot Account Missing from Permission Selector

# Robot must be created at the ORGANIZATION level, not repository level
# Verify it exists
curl -s "${QUAY_URL}/api/v1/organization/myorg/robots" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" | \
  jq '.robots[] | .name'

LDAP/SSO Users Not Triggering Default Permissions

# The "creator" in default permissions matches the Quay username
# For LDAP users, this is the LDAP uid, not email
# Verify with:
curl -s "${QUAY_URL}/api/v1/users/john_doe" \
  -H "Authorization: Bearer ${QUAY_API_TOKEN}" | jq '.username'

Permission Precedence

# If both default permission AND team permission exist:
# - Higher permission wins (write > read)
# - Explicit repo-level permissions override defaults
# - Robot with "read" default + "write" via team = "write" effective

Best Practices

  1. One robot per purpose: ci_pull (read), ci_push (write), scanner (read) β€” never share tokens across use cases
  2. Always set default permissions: Prevents ImagePullBackOff on new repos
  3. Batch-apply to existing repos: Run the script after creating default permissions
  4. Rotate robot tokens: Use rotate-quay-robot-tokens recipe for periodic rotation
  5. Audit permissions: Periodically list all prototypes and verify they match your security policy
  6. Use β€œAnyone” creator scope: Unless you have team-specific isolation requirements
  7. Prefer read over write: Only CI/CD push pipelines need write; everything else should be read-only
  8. Mirror to cluster pull secret: Use OpenShift global pull secret or namespace-level imagePullSecrets

Key Takeaways

  • Default permissions (prototypes) auto-grant access on new repositories only
  • Use the Quay API /organization/{org}/prototypes endpoint for automation
  • Separate robot accounts by role: read for clusters, write for CI/CD
  • Batch-apply to existing repos with the provided script
  • Combine with Kubernetes imagePullSecrets or OpenShift global pull secret for seamless image pulling
#quay #robot-account #permissions #registry #rbac #organization #pull-secret
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens