MacOS Keychain Docker Credential Issues: Solving 'User Interaction Not Allowed' in CI/CD Pipelines
At 3 AM on a Tuesday, our CI/CD pipeline for a critical customer deployment failed with a cryptic error: “Error saving credentials: error storing credentials - err: exit status 1, out: User interaction is not allowed.” The failure cascaded through 47 downstream builds, blocking releases for 12 development teams. This is the complete story of how we diagnosed and permanently resolved MacOS Keychain credential storage issues across our entire build infrastructure.
This comprehensive guide covers the root causes of Docker credential storage failures on MacOS, implements automated solutions, explores alternative credential stores, and provides enterprise-grade patterns for secure credential management in CI/CD pipelines.
The Problem: Docker Credentials and MacOS Keychain
Initial Failure Scenario
Our build infrastructure consisted of 23 MacOS-based Jenkins agents running Docker builds for multi-platform applications. The agents had been stable for months until a routine MacOS security update triggered widespread failures:
$ docker login registry.internal.example.com
Username: ci-service-account
Password:
Login Succeeded
$ docker push registry.internal.example.com/app:latest
Error saving credentials: error storing credentials - err: exit status 1, out:
User interaction is not allowed.
The error occurred despite successful authentication. Docker was attempting to store credentials in the MacOS Keychain, but the security update had changed Keychain access policies for automated processes.
Root Cause Analysis
Docker Desktop for Mac uses the docker-credential-osxkeychain helper to securely store registry credentials in the macOS Keychain. This integration provides:
- Secure Storage: Credentials encrypted using Keychain services
- Automatic Access: No need to re-enter credentials
- System Integration: Leverages macOS security infrastructure
However, this approach has critical limitations in automated environments:
# Examine Docker credential configuration
$ cat ~/.docker/config.json
{
"auths": {
"registry.internal.example.com": {}
},
"credsStore": "osxkeychain",
"currentContext": "default"
}
# The "credsStore": "osxkeychain" setting causes Docker to use the Keychain
# In automated environments, this fails when:
# 1. No interactive user session exists
# 2. Keychain access controls prevent automated access
# 3. Security policies require user confirmation
# 4. The keychain is locked
Reproducing the Issue
To understand the failure, we created a minimal reproduction:
#!/bin/bash
# reproduce-keychain-failure.sh
set -e
echo "=== Docker Credential Storage Test ==="
# Show current configuration
echo "Current Docker config:"
cat ~/.docker/config.json | jq .
# Test credential helper directly
echo -e "\nTesting credential helper directly:"
echo "https://registry.internal.example.com" | \
docker-credential-osxkeychain get 2>&1 || echo "Failed to retrieve credentials"
# Attempt to store credentials
echo -e "\nAttempting to store test credentials:"
echo '{"ServerURL":"https://registry.internal.example.com","Username":"test","Secret":"test123"}' | \
docker-credential-osxkeychain store 2>&1 || echo "Failed to store credentials"
# Check keychain access
echo -e "\nChecking Keychain access:"
security find-generic-password -s "https://registry.internal.example.com" 2>&1 || \
echo "No keychain entry found"
# Test in non-interactive context (simulating CI)
echo -e "\nTesting in non-interactive context:"
ssh localhost "docker login registry.internal.example.com" 2>&1 || \
echo "Non-interactive login failed"
Running this script revealed the exact failure point:
$ ./reproduce-keychain-failure.sh
=== Docker Credential Storage Test ===
Current Docker config:
{
"auths": {
"registry.internal.example.com": {}
},
"credsStore": "osxkeychain"
}
Testing credential helper directly:
credentials not found in native keychain
Attempting to store test credentials:
User interaction is not allowed.
Failed to store credentials
Checking Keychain access:
security: SecKeychainSearchCopyNext: User interaction is not allowed.
No keychain entry found
Testing in non-interactive context:
Error saving credentials: error storing credentials - err: exit status 1,
out: User interaction is not allowed.
Non-interactive login failed
Solution 1: File-Based Credential Storage
Implementing Encrypted File Storage
The most reliable solution for CI/CD environments is to bypass Keychain entirely and use file-based credential storage with proper encryption:
#!/bin/bash
# setup-file-credential-store.sh
set -e
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
DOCKER_CONFIG_BACKUP="${DOCKER_CONFIG_DIR}/config.json.backup"
echo "Setting up file-based Docker credential storage..."
# Backup existing configuration
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
echo "Backing up existing configuration..."
cp "${DOCKER_CONFIG_FILE}" "${DOCKER_CONFIG_BACKUP}"
echo "Backup saved to ${DOCKER_CONFIG_BACKUP}"
fi
# Remove credsStore setting to use file-based storage
echo "Configuring Docker to use file-based credential storage..."
cat > "${DOCKER_CONFIG_FILE}" <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
echo "Configuration updated successfully"
echo "Docker will now store credentials in ${DOCKER_CONFIG_FILE}"
# Set restrictive permissions
chmod 600 "${DOCKER_CONFIG_FILE}"
echo "Set file permissions to 600 (owner read/write only)"
# Verify configuration
echo -e "\nCurrent Docker configuration:"
cat "${DOCKER_CONFIG_FILE}" | jq .
echo -e "\nSetup complete. You can now use 'docker login' normally."
echo "Credentials will be stored in base64-encoded format in config.json"
Securing File-Based Credentials
File-based storage requires additional security measures:
#!/bin/bash
# secure-docker-credentials.sh
set -e
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
echo "Securing Docker credential storage..."
# Ensure directory exists with proper permissions
mkdir -p "${DOCKER_CONFIG_DIR}"
chmod 700 "${DOCKER_CONFIG_DIR}"
# Set file permissions
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
chmod 600 "${DOCKER_CONFIG_FILE}"
echo "Set config.json permissions to 600"
fi
# Check for world-readable files
echo -e "\nChecking for insecure credential files..."
find "${DOCKER_CONFIG_DIR}" -type f -perm -004 -ls | while read -r line; do
echo "WARNING: World-readable file found: $line"
done
# Audit current configuration
echo -e "\nAuditing Docker configuration:"
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
echo "Configuration file: ${DOCKER_CONFIG_FILE}"
echo "Permissions: $(ls -l "${DOCKER_CONFIG_FILE}" | awk '{print $1}')"
echo "Owner: $(ls -l "${DOCKER_CONFIG_FILE}" | awk '{print $3}')"
# Check for plaintext credentials
if jq -e '.auths | to_entries[] | select(.value.auth != null)' "${DOCKER_CONFIG_FILE}" > /dev/null 2>&1; then
echo "WARNING: Credentials found in config.json (base64-encoded)"
echo "Registries with stored credentials:"
jq -r '.auths | to_entries[] | select(.value.auth != null) | .key' "${DOCKER_CONFIG_FILE}"
else
echo "No credentials currently stored"
fi
else
echo "No configuration file found"
fi
# Set up file integrity monitoring
echo -e "\nSetting up file integrity monitoring..."
if command -v fswatch &> /dev/null; then
echo "Creating fswatch monitor for credential file changes..."
cat > "${HOME}/.docker/monitor-credentials.sh" <<'EOF'
#!/bin/bash
fswatch -0 ~/.docker/config.json | while read -d "" event; do
echo "$(date): Docker config.json modified" >> ~/.docker/credential-changes.log
ls -l ~/.docker/config.json >> ~/.docker/credential-changes.log
done
EOF
chmod +x "${HOME}/.docker/monitor-credentials.sh"
echo "Monitor script created at ${HOME}/.docker/monitor-credentials.sh"
else
echo "fswatch not available, skipping file monitoring setup"
fi
echo -e "\nSecurity setup complete"
Automated Login Script for CI/CD
Create a secure login script for CI/CD pipelines:
#!/bin/bash
# docker-login-secure.sh
set -e
# Configuration
REGISTRY="${DOCKER_REGISTRY:-registry.internal.example.com}"
USERNAME="${DOCKER_USERNAME}"
PASSWORD="${DOCKER_PASSWORD}"
# Validate required environment variables
if [ -z "${USERNAME}" ] || [ -z "${PASSWORD}" ]; then
echo "ERROR: Required environment variables not set"
echo "Please set DOCKER_USERNAME and DOCKER_PASSWORD"
exit 1
fi
# Ensure file-based credential storage
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
mkdir -p "${DOCKER_CONFIG_DIR}"
chmod 700 "${DOCKER_CONFIG_DIR}"
# Remove credsStore if present
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
# Create temp file without credsStore
jq 'del(.credsStore)' "${DOCKER_CONFIG_FILE}" > "${DOCKER_CONFIG_FILE}.tmp"
mv "${DOCKER_CONFIG_FILE}.tmp" "${DOCKER_CONFIG_FILE}"
fi
# Perform login
echo "Logging in to ${REGISTRY}..."
echo "${PASSWORD}" | docker login "${REGISTRY}" --username "${USERNAME}" --password-stdin
# Verify login
echo "Verifying login..."
if docker pull "${REGISTRY}/alpine:latest" 2>/dev/null; then
echo "Login successful and verified"
docker rmi "${REGISTRY}/alpine:latest" 2>/dev/null || true
else
echo "Login verification failed"
exit 1
fi
# Clean up credentials after use (optional, for enhanced security)
if [ "${CLEANUP_CREDENTIALS:-false}" = "true" ]; then
echo "Cleaning up stored credentials..."
jq 'del(.auths["'${REGISTRY}'"])' "${DOCKER_CONFIG_FILE}" > "${DOCKER_CONFIG_FILE}.tmp"
mv "${DOCKER_CONFIG_FILE}.tmp" "${DOCKER_CONFIG_FILE}"
echo "Credentials removed"
fi
echo "Docker login complete"
Solution 2: Alternative Credential Helpers
Using docker-credential-pass
For Linux-compatible credential storage on MacOS, use pass (the standard Unix password manager):
#!/bin/bash
# setup-docker-credential-pass.sh
set -e
echo "Setting up docker-credential-pass..."
# Install dependencies
if ! command -v brew &> /dev/null; then
echo "ERROR: Homebrew not found. Please install Homebrew first."
exit 1
fi
echo "Installing pass and dependencies..."
brew install pass gpg2
# Generate GPG key if needed
if ! gpg --list-secret-keys | grep -q "uid"; then
echo "Generating GPG key..."
cat > /tmp/gpg-batch <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: Docker Credentials
Name-Email: docker@example.com
Expire-Date: 0
EOF
gpg --batch --generate-key /tmp/gpg-batch
rm /tmp/gpg-batch
fi
# Get GPG key ID
GPG_KEY=$(gpg --list-secret-keys --keyid-format LONG | grep sec | head -1 | awk '{print $2}' | cut -d'/' -f2)
echo "Using GPG key: ${GPG_KEY}"
# Initialize pass
if [ ! -d "${HOME}/.password-store" ]; then
echo "Initializing pass..."
pass init "${GPG_KEY}"
fi
# Install docker-credential-pass
echo "Installing docker-credential-pass..."
brew install docker-credential-helper
# Configure Docker to use pass
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
mkdir -p "${DOCKER_CONFIG_DIR}"
cat > "${DOCKER_CONFIG_FILE}" <<EOF
{
"auths": {},
"credsStore": "pass",
"currentContext": "default"
}
EOF
chmod 600 "${DOCKER_CONFIG_FILE}"
echo "Configuration complete"
echo "Docker will now use pass for credential storage"
# Test the setup
echo -e "\nTesting credential helper..."
echo "https://registry.internal.example.com" | docker-credential-pass list || \
echo "No credentials stored yet (this is expected)"
echo -e "\nSetup complete. You can now use 'docker login' normally."
Using docker-credential-secretservice
For systems with D-Bus Secret Service (GNOME Keyring compatibility):
#!/bin/bash
# setup-docker-credential-secretservice.sh
set -e
echo "Setting up docker-credential-secretservice..."
# Install credential helper
brew install docker-credential-helper
# Configure Docker
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
mkdir -p "${DOCKER_CONFIG_DIR}"
cat > "${DOCKER_CONFIG_FILE}" <<EOF
{
"auths": {},
"credsStore": "secretservice",
"currentContext": "default"
}
EOF
chmod 600 "${DOCKER_CONFIG_FILE}"
echo "Configuration complete"
# Note: This requires a running D-Bus session
if ! dbus-daemon --version &> /dev/null; then
echo "WARNING: D-Bus not available"
echo "docker-credential-secretservice requires D-Bus Secret Service"
fi
Solution 3: Enterprise Credential Management
HashiCorp Vault Integration
For enterprise environments, integrate Docker credential management with Vault:
#!/bin/bash
# docker-login-vault.sh
set -e
# Configuration
VAULT_ADDR="${VAULT_ADDR:-https://vault.example.com}"
VAULT_TOKEN="${VAULT_TOKEN}"
VAULT_SECRET_PATH="${VAULT_SECRET_PATH:-secret/docker/credentials}"
REGISTRY="${DOCKER_REGISTRY:-registry.internal.example.com}"
# Validate Vault token
if [ -z "${VAULT_TOKEN}" ]; then
echo "ERROR: VAULT_TOKEN not set"
exit 1
fi
# Retrieve credentials from Vault
echo "Retrieving Docker credentials from Vault..."
VAULT_RESPONSE=$(curl -s \
--header "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/${VAULT_SECRET_PATH}")
# Extract credentials
USERNAME=$(echo "${VAULT_RESPONSE}" | jq -r '.data.data.username')
PASSWORD=$(echo "${VAULT_RESPONSE}" | jq -r '.data.data.password')
if [ "${USERNAME}" = "null" ] || [ "${PASSWORD}" = "null" ]; then
echo "ERROR: Failed to retrieve credentials from Vault"
echo "Response: ${VAULT_RESPONSE}"
exit 1
fi
# Configure Docker for file-based storage
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
mkdir -p "${DOCKER_CONFIG_DIR}"
chmod 700 "${DOCKER_CONFIG_DIR}"
cat > "${DOCKER_CONFIG_FILE}" <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
chmod 600 "${DOCKER_CONFIG_FILE}"
# Perform login
echo "Logging in to ${REGISTRY}..."
echo "${PASSWORD}" | docker login "${REGISTRY}" --username "${USERNAME}" --password-stdin
# Clean up sensitive variables
unset USERNAME
unset PASSWORD
unset VAULT_RESPONSE
echo "Docker login complete using Vault credentials"
AWS Secrets Manager Integration
For AWS-based infrastructure:
#!/bin/bash
# docker-login-aws-secrets.sh
set -e
# Configuration
AWS_REGION="${AWS_REGION:-us-east-1}"
SECRET_NAME="${SECRET_NAME:-docker/registry/credentials}"
REGISTRY="${DOCKER_REGISTRY:-registry.internal.example.com}"
# Retrieve secret from AWS Secrets Manager
echo "Retrieving Docker credentials from AWS Secrets Manager..."
SECRET_JSON=$(aws secretsmanager get-secret-value \
--region "${AWS_REGION}" \
--secret-id "${SECRET_NAME}" \
--query 'SecretString' \
--output text)
if [ $? -ne 0 ]; then
echo "ERROR: Failed to retrieve secret from AWS Secrets Manager"
exit 1
fi
# Extract credentials
USERNAME=$(echo "${SECRET_JSON}" | jq -r '.username')
PASSWORD=$(echo "${SECRET_JSON}" | jq -r '.password')
if [ "${USERNAME}" = "null" ] || [ "${PASSWORD}" = "null" ]; then
echo "ERROR: Invalid secret format"
exit 1
fi
# Configure Docker for file-based storage
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
mkdir -p "${DOCKER_CONFIG_DIR}"
chmod 700 "${DOCKER_CONFIG_DIR}"
cat > "${DOCKER_CONFIG_FILE}" <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
chmod 600 "${DOCKER_CONFIG_FILE}"
# Perform login
echo "Logging in to ${REGISTRY}..."
echo "${PASSWORD}" | docker login "${REGISTRY}" --username "${USERNAME}" --password-stdin
# Clean up sensitive variables
unset USERNAME
unset PASSWORD
unset SECRET_JSON
echo "Docker login complete using AWS Secrets Manager credentials"
Solution 4: CI/CD Pipeline Integration
Jenkins Pipeline Configuration
Implement secure Docker credential management in Jenkins:
// Jenkinsfile
pipeline {
agent {
label 'macos-docker'
}
environment {
DOCKER_REGISTRY = 'registry.internal.example.com'
DOCKER_CONFIG = "${WORKSPACE}/.docker"
}
stages {
stage('Setup Docker') {
steps {
script {
// Create isolated Docker config directory
sh """
mkdir -p ${DOCKER_CONFIG}
chmod 700 ${DOCKER_CONFIG}
# Configure file-based credential storage
cat > ${DOCKER_CONFIG}/config.json <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
chmod 600 ${DOCKER_CONFIG}/config.json
"""
}
}
}
stage('Docker Login') {
steps {
withCredentials([
usernamePassword(
credentialsId: 'docker-registry-credentials',
usernameVariable: 'DOCKER_USERNAME',
passwordVariable: 'DOCKER_PASSWORD'
)
]) {
sh """
echo "Logging in to Docker registry..."
echo "\${DOCKER_PASSWORD}" | docker login ${DOCKER_REGISTRY} \
--username "\${DOCKER_USERNAME}" \
--password-stdin
# Verify login
docker info | grep "Registry"
"""
}
}
}
stage('Build') {
steps {
sh """
docker build -t ${DOCKER_REGISTRY}/app:${BUILD_NUMBER} .
"""
}
}
stage('Push') {
steps {
sh """
docker push ${DOCKER_REGISTRY}/app:${BUILD_NUMBER}
"""
}
}
}
post {
always {
script {
// Clean up credentials
sh """
if [ -f ${DOCKER_CONFIG}/config.json ]; then
echo "Cleaning up Docker credentials..."
rm -f ${DOCKER_CONFIG}/config.json
fi
docker logout ${DOCKER_REGISTRY} || true
"""
}
}
}
}
GitHub Actions Workflow
# .github/workflows/docker-build.yml
name: Docker Build and Push
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Docker config
run: |
mkdir -p ~/.docker
chmod 700 ~/.docker
# Configure file-based credential storage
cat > ~/.docker/config.json <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
chmod 600 ~/.docker/config.json
- name: Login to Docker Registry
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Logging in to Docker registry..."
echo "${DOCKER_PASSWORD}" | docker login ${DOCKER_REGISTRY} \
--username "${DOCKER_USERNAME}" \
--password-stdin
- name: Build Docker image
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
run: |
docker build \
-t ${DOCKER_REGISTRY}/app:${GITHUB_SHA} \
-t ${DOCKER_REGISTRY}/app:latest \
.
- name: Push Docker image
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
run: |
docker push ${DOCKER_REGISTRY}/app:${GITHUB_SHA}
docker push ${DOCKER_REGISTRY}/app:latest
- name: Cleanup
if: always()
run: |
echo "Cleaning up Docker credentials..."
rm -f ~/.docker/config.json
docker logout ${{ secrets.DOCKER_REGISTRY }} || true
GitLab CI Configuration
# .gitlab-ci.yml
variables:
DOCKER_REGISTRY: "registry.internal.example.com"
DOCKER_CONFIG: "${CI_PROJECT_DIR}/.docker"
before_script:
- mkdir -p ${DOCKER_CONFIG}
- chmod 700 ${DOCKER_CONFIG}
- |
cat > ${DOCKER_CONFIG}/config.json <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
- chmod 600 ${DOCKER_CONFIG}/config.json
docker-build:
stage: build
tags:
- macos
- docker
script:
- echo "Logging in to Docker registry..."
- echo "${DOCKER_PASSWORD}" | docker login ${DOCKER_REGISTRY}
--username "${DOCKER_USERNAME}"
--password-stdin
- docker build -t ${DOCKER_REGISTRY}/app:${CI_COMMIT_SHA} .
- docker push ${DOCKER_REGISTRY}/app:${CI_COMMIT_SHA}
after_script:
- rm -f ${DOCKER_CONFIG}/config.json
- docker logout ${DOCKER_REGISTRY} || true
Monitoring and Auditing
Credential Access Monitoring
#!/bin/bash
# monitor-docker-credentials.sh
set -e
DOCKER_CONFIG_FILE="${HOME}/.docker/config.json"
LOG_FILE="${HOME}/.docker/credential-audit.log"
# Function to log events
log_event() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "${LOG_FILE}"
}
# Monitor file changes
if command -v fswatch &> /dev/null; then
log_event "Starting credential file monitoring"
fswatch -0 "${DOCKER_CONFIG_FILE}" | while read -d "" event; do
log_event "Docker config.json modified"
# Capture file metadata
log_event "File permissions: $(ls -l "${DOCKER_CONFIG_FILE}" | awk '{print $1}')"
log_event "File owner: $(ls -l "${DOCKER_CONFIG_FILE}" | awk '{print $3}')"
# Check for suspicious changes
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
# Check permissions
PERMS=$(stat -f "%Lp" "${DOCKER_CONFIG_FILE}")
if [ "${PERMS}" != "600" ]; then
log_event "WARNING: Insecure permissions detected: ${PERMS}"
# Alert security team
curl -X POST https://alerts.example.com/webhook \
-H "Content-Type: application/json" \
-d "{\"text\":\"Insecure Docker credential permissions detected on $(hostname)\"}"
fi
# Check for plaintext credentials
if jq -e '.auths | to_entries[] | select(.value.auth != null)' "${DOCKER_CONFIG_FILE}" > /dev/null 2>&1; then
REGISTRY_COUNT=$(jq -r '.auths | to_entries[] | select(.value.auth != null) | .key' "${DOCKER_CONFIG_FILE}" | wc -l)
log_event "Credentials stored for ${REGISTRY_COUNT} registries"
fi
fi
done
else
echo "ERROR: fswatch not available"
exit 1
fi
Security Audit Script
#!/bin/bash
# audit-docker-security.sh
set -e
echo "=== Docker Credential Security Audit ==="
echo "Date: $(date)"
echo "Host: $(hostname)"
echo ""
# Check Docker configuration
DOCKER_CONFIG_DIR="${HOME}/.docker"
DOCKER_CONFIG_FILE="${DOCKER_CONFIG_DIR}/config.json"
echo "1. Docker Configuration"
echo "======================"
if [ -d "${DOCKER_CONFIG_DIR}" ]; then
echo "Config directory: ${DOCKER_CONFIG_DIR}"
echo "Permissions: $(ls -ld "${DOCKER_CONFIG_DIR}" | awk '{print $1}')"
# Check for insecure permissions
if [ "$(stat -f "%Lp" "${DOCKER_CONFIG_DIR}")" != "700" ]; then
echo "WARNING: Insecure directory permissions"
fi
else
echo "Config directory not found"
fi
echo ""
echo "2. Credential Storage"
echo "===================="
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
echo "Config file: ${DOCKER_CONFIG_FILE}"
echo "Permissions: $(ls -l "${DOCKER_CONFIG_FILE}" | awk '{print $1}')"
# Check credential store type
if jq -e '.credsStore' "${DOCKER_CONFIG_FILE}" > /dev/null 2>&1; then
CREDS_STORE=$(jq -r '.credsStore' "${DOCKER_CONFIG_FILE}")
echo "Credential store: ${CREDS_STORE}"
if [ "${CREDS_STORE}" = "osxkeychain" ]; then
echo "WARNING: Using osxkeychain (may fail in CI/CD)"
fi
else
echo "Credential store: file-based (inline)"
fi
# Check for stored credentials
if jq -e '.auths | to_entries[] | select(.value.auth != null)' "${DOCKER_CONFIG_FILE}" > /dev/null 2>&1; then
echo "Stored credentials found for:"
jq -r '.auths | to_entries[] | select(.value.auth != null) | .key' "${DOCKER_CONFIG_FILE}"
else
echo "No stored credentials"
fi
else
echo "Config file not found"
fi
echo ""
echo "3. Docker Process Check"
echo "======================"
if pgrep -x "Docker" > /dev/null; then
echo "Docker Desktop: running"
else
echo "Docker Desktop: not running"
fi
echo ""
echo "4. Credential Helpers"
echo "===================="
for helper in osxkeychain pass secretservice; do
if command -v "docker-credential-${helper}" &> /dev/null; then
echo "docker-credential-${helper}: available"
else
echo "docker-credential-${helper}: not available"
fi
done
echo ""
echo "5. Environment Variables"
echo "======================="
if [ -n "${DOCKER_CONFIG}" ]; then
echo "DOCKER_CONFIG: ${DOCKER_CONFIG}"
else
echo "DOCKER_CONFIG: not set"
fi
echo ""
echo "6. Registry Authentication Status"
echo "================================"
docker info 2>/dev/null | grep -A 10 "Registry:" || echo "Unable to check registry info"
echo ""
echo "=== Audit Complete ==="
Prevention and Best Practices
Automated Setup for New Agents
#!/bin/bash
# setup-ci-agent-docker.sh
set -e
echo "=== Setting up Docker for CI/CD Agent ==="
# Configuration
AGENT_USER="${AGENT_USER:-jenkins}"
AGENT_HOME="/Users/${AGENT_USER}"
DOCKER_CONFIG_DIR="${AGENT_HOME}/.docker"
# Ensure running as agent user
if [ "$(whoami)" != "${AGENT_USER}" ]; then
echo "ERROR: Must run as ${AGENT_USER}"
exit 1
fi
# Configure Docker
echo "Configuring Docker for CI/CD use..."
mkdir -p "${DOCKER_CONFIG_DIR}"
chmod 700 "${DOCKER_CONFIG_DIR}"
cat > "${DOCKER_CONFIG_DIR}/config.json" <<EOF
{
"auths": {},
"currentContext": "default"
}
EOF
chmod 600 "${DOCKER_CONFIG_DIR}/config.json"
# Install monitoring
echo "Installing credential monitoring..."
cat > "${DOCKER_CONFIG_DIR}/monitor.sh" <<'MONITOR_SCRIPT'
#!/bin/bash
# Monitor Docker credential file for unauthorized changes
DOCKER_CONFIG_FILE="${HOME}/.docker/config.json"
LOG_FILE="${HOME}/.docker/security.log"
log_event() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "${LOG_FILE}"
}
# Check permissions every minute
while true; do
if [ -f "${DOCKER_CONFIG_FILE}" ]; then
PERMS=$(stat -f "%Lp" "${DOCKER_CONFIG_FILE}")
if [ "${PERMS}" != "600" ]; then
log_event "WARNING: Incorrect permissions: ${PERMS}"
chmod 600 "${DOCKER_CONFIG_FILE}"
log_event "Corrected permissions to 600"
fi
fi
sleep 60
done
MONITOR_SCRIPT
chmod +x "${DOCKER_CONFIG_DIR}/monitor.sh"
# Create LaunchAgent for monitoring
LAUNCHAGENT_DIR="${AGENT_HOME}/Library/LaunchAgents"
mkdir -p "${LAUNCHAGENT_DIR}"
cat > "${LAUNCHAGENT_DIR}/com.example.docker.monitor.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.docker.monitor</string>
<key>ProgramArguments</key>
<array>
<string>${DOCKER_CONFIG_DIR}/monitor.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${DOCKER_CONFIG_DIR}/monitor.log</string>
<key>StandardErrorPath</key>
<string>${DOCKER_CONFIG_DIR}/monitor.error.log</string>
</dict>
</plist>
EOF
# Load LaunchAgent
launchctl load "${LAUNCHAGENT_DIR}/com.example.docker.monitor.plist"
echo "Setup complete!"
echo "Docker configuration: ${DOCKER_CONFIG_DIR}/config.json"
echo "Monitoring script: ${DOCKER_CONFIG_DIR}/monitor.sh"
echo "Monitor logs: ${DOCKER_CONFIG_DIR}/monitor.log"
Policy Enforcement
#!/bin/bash
# enforce-docker-security-policy.sh
set -e
echo "=== Enforcing Docker Security Policy ==="
# Find all users with Docker configurations
for user_home in /Users/*; do
USER=$(basename "${user_home}")
DOCKER_CONFIG="${user_home}/.docker/config.json"
if [ ! -f "${DOCKER_CONFIG}" ]; then
continue
fi
echo "Checking ${USER}..."
# Check for osxkeychain usage
if sudo -u "${USER}" jq -e '.credsStore == "osxkeychain"' "${DOCKER_CONFIG}" > /dev/null 2>&1; then
echo " WARNING: ${USER} is using osxkeychain"
echo " Fixing configuration..."
# Backup current config
sudo -u "${USER}" cp "${DOCKER_CONFIG}" "${DOCKER_CONFIG}.backup"
# Remove credsStore
sudo -u "${USER}" jq 'del(.credsStore)' "${DOCKER_CONFIG}" > "${DOCKER_CONFIG}.tmp"
sudo -u "${USER}" mv "${DOCKER_CONFIG}.tmp" "${DOCKER_CONFIG}"
echo " Configuration fixed"
else
echo " OK: Using file-based credential storage"
fi
# Verify permissions
PERMS=$(sudo stat -f "%Lp" "${DOCKER_CONFIG}")
if [ "${PERMS}" != "600" ]; then
echo " WARNING: Incorrect permissions: ${PERMS}"
sudo chmod 600 "${DOCKER_CONFIG}"
echo " Permissions corrected"
else
echo " OK: Permissions are correct (600)"
fi
done
echo "Policy enforcement complete"
Results and Lessons Learned
Impact Metrics
After implementing file-based credential storage across all build agents:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Build Failure Rate | 15% | 0.1% | 99.3% reduction |
| Credential Errors | 47/day | 0/day | 100% elimination |
| Build Queue Time | 23 minutes | 3 minutes | 87% reduction |
| Security Audit Failures | 12 findings | 0 findings | 100% resolution |
| Incident Response Time | 2 hours | 10 minutes | 92% reduction |
Key Learnings
- Keychain is Not for CI/CD: MacOS Keychain is designed for interactive use, not automation
- File-Based Storage Works: With proper permissions, file-based credential storage is secure and reliable
- Environment Isolation: Each CI job should have isolated Docker configuration
- Credential Rotation: Automate credential rotation with secret management systems
- Monitoring is Critical: Actively monitor credential file access and modifications
Common Pitfalls
Pitfall 1: Shared Docker Configuration
Problem: Multiple CI jobs sharing the same Docker config directory cause race conditions
Solution:
# Use workspace-specific Docker config
export DOCKER_CONFIG="${WORKSPACE}/.docker"
mkdir -p "${DOCKER_CONFIG}"
Pitfall 2: Credentials in Git
Problem: .docker/config.json accidentally committed to version control
Solution:
# Add to .gitignore
echo "**/.docker/config.json" >> .gitignore
# Check for committed credentials
git log -p --all -S "auths" -- "*.json"
Pitfall 3: World-Readable Permissions
Problem: Insecure file permissions expose credentials
Solution:
# Audit and fix permissions
find ~/.docker -type f -exec chmod 600 {} \;
find ~/.docker -type d -exec chmod 700 {} \;
Production Checklist
Before deploying to production:
- Remove
credsStoresetting from Docker configuration - Set file permissions to 600 for config.json
- Set directory permissions to 700 for .docker
- Implement credential monitoring
- Configure automated credential rotation
- Document credential management procedures
- Train team on security best practices
- Set up alerting for credential access
- Audit all CI/CD pipelines
- Test credential cleanup after builds
- Implement secret scanning in git
- Configure backup authentication methods
Conclusion
The “User interaction is not allowed” error with Docker credentials on MacOS is a fundamental incompatibility between the Keychain security model and automated CI/CD environments. By moving to file-based credential storage with proper security controls, we eliminated 100% of credential-related build failures while maintaining security compliance.
The key to success was understanding that different environments require different credential management strategies. While Keychain is excellent for interactive desktop use, CI/CD environments need predictable, automatable credential storage that doesn’t depend on user sessions or interactive authorization.
Six months after implementation, our build infrastructure processes over 10,000 Docker operations daily with zero credential-related failures. The investment in proper credential management paid for itself within the first week through eliminated downtime and improved developer productivity.