Skip to main content

ICP Platform - Docker Integration Guide

Complete guide for integrating ICP (Identity Control Plane) workload identity with Docker containers and Docker Compose.

Table of Contents


Prerequisites

System Requirements

  • Docker Engine: 20.10+ (with compose plugin)
  • Docker Compose: 2.0+ (or standalone 1.29+)
  • Operating System: Linux, macOS, Windows with WSL2
  • RAM: 2GB minimum (agent + workloads)
  • Network: Outbound HTTPS (443) to ICP Server

Installation

# Install Docker Engine
curl -fsSL https://get.docker.com | sh

# Install Docker Compose (if not included)
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Verify installation
docker --version
docker compose version

Credentials

You'll need:

  1. Tenant ID - Provided by AuthSec
  2. ICP Server URL - Provided by AuthSec
  3. Node ID - Unique identifier for this Docker host (e.g., docker-prod-host-01)

Architecture Overview

Docker Deployment Architecture

┌──────────────────────────────────────────────────────────────────────┐
│ Docker Host │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ ICP Agent Container │ │
│ │ │ │
│ │ Image: icp-agent:latest │ │
│ │ Network: host / bridge │ │
│ │ Volumes: │ │
│ │ - agent-socket (shared with workloads) │ │
│ │ - /var/run/docker.sock (for Docker API access) │ │
│ │ │ │
│ │ Env: │ │
│ │ ICP_AGENT_AGENT__TENANT_ID=your-tenant-id-here │ │
│ │ ICP_AGENT_AGENT__NODE_ID=docker-prod-host-01 │ │
│ │ ICP_AGENT_ICP_SERVICE__ADDRESS=https://icp-server... │ │
│ └────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ │ Unix Socket │
│ │ (agent-socket volume) │
│ │ │
│ ┌──────────────────────┴────────────────────┐ │
│ │ │ │
│ ┌──────▼─────────────────┐ ┌─────────────────▼──────────┐ │
│ │ Workload Container A │ │ Workload Container B │ │
│ │ (web-service) │ │ (api-service) │ │
│ │ │ │ │ │
│ │ Volumes: │ │ Volumes: │ │
│ │ - agent-socket │ │ - agent-socket │ │
│ │ │ │ │ │
│ │ Env: │ │ Env: │ │
│ │ DOCKER_LABEL_APP= │ │ DOCKER_LABEL_APP= │ │
│ │ web-service │ │ api-service │ │
│ │ DOCKER_CONTAINER_NAME│ │ DOCKER_CONTAINER_NAME │ │
│ │ DOCKER_IMAGE_NAME │ │ DOCKER_IMAGE_NAME │ │
│ │ │ │ │ │
│ │ Labels: │ │ Labels: │ │
│ │ app=web-service │ │ app=api-service │ │
│ │ env=production │ │ env=production │ │
│ └────────────────────────┘ └────────────────────────────┘ │
│ │
└──────────────────────────────────┬───────────────────────────────────┘

│ HTTPS + mTLS

┌──────────────────────────────┐
│ ICP Server (SaaS) │
│ - Agent SVID Issuance │
│ - Workload SVID Issuance │
└──────────────────────────────┘

How Docker Attestation Works

The agent uses the Docker Plugin to collect container metadata as selectors:

SelectorDescriptionExample
docker:label:<key>Container label"docker:label:app": "web-service"
docker:container-idFull container ID"abc123def456..."
docker:container-nameContainer name"web-service"
docker:image-nameImage name"my-app:latest"
docker:image-idImage ID"sha256:abc123..."

Metadata is collected via:

  1. Docker API (agent queries /var/run/docker.sock)
  2. Environment Variables (SDK sends metadata via gRPC headers)

Installation

Quick Start with Docker Compose

This is the recommended method for most deployments.

Step 1: Create docker-compose.yml

mkdir icp-demo
cd icp-demo

cat > docker-compose.yml <<'EOF'
version: '3.8'

services:
# ICP Agent
icp-agent:
image: your-docker-registry.example.com/icp-agent:latest
container_name: icp-agent
hostname: icp-agent-docker

environment:
# Tenant configuration
- ICP_AGENT_AGENT__TENANT_ID=your-tenant-id-here
- ICP_AGENT_AGENT__NODE_ID=docker-prod-host-01

# ICP Server connection
- ICP_AGENT_ICP_SERVICE__ADDRESS=https://dev.api.authsec.dev/spiresvc

# Attestation
- ICP_AGENT_ATTESTATION__TYPE=auto

# Logging
- ICP_AGENT_LOGGING__LEVEL=info
- ICP_AGENT_LOGGING__FORMAT=json

volumes:
# Shared socket for workloads
- agent-socket:/run/spire/sockets

# Docker API access (for container attestation)
- /var/run/docker.sock:/var/run/docker.sock:ro

networks:
- icp-network

restart: unless-stopped

healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

# Example Workload A (Web Service)
web-service:
build: ./web-service
container_name: web-service
hostname: web-service

depends_on:
icp-agent:
condition: service_healthy

environment:
# SPIFFE socket
- SPIFFE_ENDPOINT_SOCKET=unix:///run/spire/sockets/agent.sock

# Docker metadata (for attestation)
- DOCKER_CONTAINER_ID=${HOSTNAME}
- DOCKER_CONTAINER_NAME=web-service
- DOCKER_IMAGE_NAME=web-service:latest

# Application config
- PORT=8080
- LOG_LEVEL=info

volumes:
# Mount agent socket
- agent-socket:/run/spire/sockets:ro

networks:
- icp-network

ports:
- "8080:8080"

labels:
# CRITICAL: These labels are used for attestation
- "app=web-service"
- "env=production"
- "version=v1"

restart: unless-stopped

# Example Workload B (API Service)
api-service:
build: ./api-service
container_name: api-service
hostname: api-service

depends_on:
icp-agent:
condition: service_healthy

environment:
- SPIFFE_ENDPOINT_SOCKET=unix:///run/spire/sockets/agent.sock
- DOCKER_CONTAINER_ID=${HOSTNAME}
- DOCKER_CONTAINER_NAME=api-service
- DOCKER_IMAGE_NAME=api-service:latest
- PORT=8443

volumes:
- agent-socket:/run/spire/sockets:ro

networks:
- icp-network

ports:
- "8443:8443"

labels:
- "app=api-service"
- "env=production"
- "version=v1"

restart: unless-stopped

volumes:
agent-socket:
driver: local

networks:
icp-network:
driver: bridge
EOF

Step 2: Create Example Workload

Directory Structure:

icp-demo/
├── docker-compose.yml
├── web-service/
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
└── api-service/
├── Dockerfile
├── app.py
└── requirements.txt

File: web-service/requirements.txt

git+https://github.com/authsec-ai/sdk-authsec.git
fastapi
uvicorn
httpx

File: web-service/app.py

import asyncio
from authsec_sdk import QuickStartSVID
from fastapi import FastAPI
import uvicorn
import httpx

app = FastAPI()
svid = None

@app.on_event("startup")
async def startup():
global svid
svid = await QuickStartSVID.initialize()
print(f"✅ Web Service authenticated as: {svid.spiffe_id}")

@app.get("/healthz")
async def health():
return {"status": "healthy"}

@app.get("/")
async def root():
return {
"service": "web-service",
"spiffe_id": svid.spiffe_id if svid else None
}

@app.get("/call-api")
async def call_api():
"""Call API service with mTLS"""
ssl_context = svid.create_ssl_context_for_client()

async with httpx.AsyncClient(verify=ssl_context) as client:
response = await client.get("https://api-service:8443/api/data")
return response.json()

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)

File: web-service/Dockerfile

FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY app.py .

# Run as non-root user
RUN useradd -m -u 1000 appuser
USER appuser

EXPOSE 8080

CMD ["python", "app.py"]

Step 3: Start Services

# Start all services
docker compose up -d

# Check status
docker compose ps

# Check logs
docker compose logs -f icp-agent

# Verify agent health
curl http://localhost:8080/healthz

Expected output:

{"status":"healthy"}

Step 4: Register Workloads

# Set variables
export ICP_SERVER_URL="https://dev.api.authsec.dev/spiresvc"
export TENANT_ID="your-tenant-id-here"
export NODE_ID="docker-prod-host-01"

# Register web-service
curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-H "Content-Type: application/json" \
-d '{
"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id",
"selectors": {
"docker:label:app": "web-service",
"docker:label:env": "production"
},
"ttl": 3600
}'

# Register api-service
curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-H "Content-Type: application/json" \
-d '{
"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id",
"selectors": {
"docker:label:app": "api-service",
"docker:label:env": "production"
},
"ttl": 3600
}'

Step 5: Test

# Test web service
curl http://localhost:8080/

# Expected output:
# {
# "service": "web-service",
# "spiffe_id": "spiffe://your-trust-domain.example.com/workload/web-service"
# }

# Test mTLS communication
curl http://localhost:8080/call-api

Workload Registration

Understanding Selectors

Docker workloads are matched using container metadata.

Common Selector Strategies

Best for production deployments:

# Register by app label
curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-H "Content-Type: application/json" \
-d '{
"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id",
"selectors": {
"docker:label:app": "web-app"
}
}'

Strategy 2: By Multiple Labels

For environment-specific workloads:

curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-H "Content-Type: application/json" \
-d '{
"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id",
"selectors": {
"docker:label:app": "web-app",
"docker:label:env": "production",
"docker:label:version": "v1"
}
}'

Strategy 3: By Container Name

For simple deployments:

curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-H "Content-Type: application/json" \
-d '{
"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id",
"selectors": {
"docker:container-name": "nginx-proxy"
}
}'

Strategy 4: By Image Name

For workloads with specific images:

curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-H "Content-Type: application/json" \
-d '{
"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id",
"selectors": {
"docker:image-name": "postgres:14-alpine"
}
}'

Multi-Service Example

Complete docker-compose.yml with multiple services:

version: '3.8'

services:
icp-agent:
# ... (agent config as above)

frontend:
image: my-registry.example.com/frontend:latest
labels:
- "app=frontend"
- "tier=presentation"
- "env=production"
environment:
- SPIFFE_ENDPOINT_SOCKET=unix:///run/spire/sockets/agent.sock
- DOCKER_LABEL_APP=frontend
- DOCKER_LABEL_TIER=presentation
volumes:
- agent-socket:/run/spire/sockets:ro
networks:
- icp-network

backend:
image: my-registry.example.com/backend:latest
labels:
- "app=backend"
- "tier=business"
- "env=production"
environment:
- SPIFFE_ENDPOINT_SOCKET=unix:///run/spire/sockets/agent.sock
- DOCKER_LABEL_APP=backend
- DOCKER_LABEL_TIER=business
volumes:
- agent-socket:/run/spire/sockets:ro
networks:
- icp-network

database:
image: postgres:14-alpine
labels:
- "app=postgres"
- "tier=data"
- "env=production"
environment:
- POSTGRES_PASSWORD=secure-password
- SPIFFE_ENDPOINT_SOCKET=unix:///run/spire/sockets/agent.sock
- DOCKER_LABEL_APP=postgres
- DOCKER_LABEL_TIER=data
volumes:
- agent-socket:/run/spire/sockets:ro
- postgres-data:/var/lib/postgresql/data
networks:
- icp-network

volumes:
agent-socket:
postgres-data:

networks:
icp-network:

Register all services:

# Frontend
curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-d '{"spiffe_id": "spiffe://your-spiffe-id","parent_id": "spiffe://your-parent-id", "selectors": {"docker:label:app": "frontend"}}'

# Backend
curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-d '{"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id", "selectors": {"docker:label:app": "backend"}}'

# Database
curl -X POST "${ICP_SERVER_URL}/api/v1/workloads" \
-d '{"spiffe_id": "spiffe://your-spiffe-id",
"parent_id": "spiffe://your-parent-id", "selectors": {"docker:label:app": "postgres"}}'

SDK Integration

Installation in Dockerfile

FROM python:3.11-slim

WORKDIR /app

# Install SDK from GitHub
RUN pip install git+https://github.com/authsec-ai/sdk-authsec.git

# Install application dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt

# Copy application
COPY . .

CMD ["python", "app.py"]

Quick Start Example

import asyncio
from authsec_sdk import QuickStartSVID

async def main():
# Initialize SDK
svid = await QuickStartSVID.initialize()

print(f"✅ Container authenticated as: {svid.spiffe_id}")
print(f"📜 Certificate expires: {svid.expires_at}")

# Use in your application
# ...

if __name__ == "__main__":
asyncio.run(main())

Client-Side mTLS

from authsec_sdk import QuickStartSVID
import httpx
import asyncio

async def call_backend():
svid = await QuickStartSVID.initialize()

# Create fresh SSL context (handles cert renewal)
ssl_context = svid.create_ssl_context_for_client()

async with httpx.AsyncClient(verify=ssl_context) as client:
response = await client.post(
"https://backend:8443/api/endpoint",
json={"key": "value"}
)
return response.json()

if __name__ == "__main__":
result = asyncio.run(call_backend())
print(result)

Server-Side mTLS

from fastapi import FastAPI
from authsec_sdk import QuickStartSVID
import uvicorn
import asyncio

app = FastAPI()
svid = None

@app.on_event("startup")
async def startup():
global svid
svid = await QuickStartSVID.initialize()

@app.get("/api/endpoint")
async def endpoint():
return {"status": "success"}

if __name__ == "__main__":
# Run with mTLS
uvicorn.run(
app,
host="0.0.0.0",
port=8443,
ssl_certfile=svid.cert_file_path,
ssl_keyfile=svid.key_file_path,
ssl_ca_certs=svid.ca_file_path
)

Production Deployment

Docker Swarm

For production scale-out:

version: '3.8'

services:
icp-agent:
image: your-docker-registry.example.com/icp-agent:latest
deploy:
mode: global # Run on every node
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 128M
environment:
- ICP_AGENT_AGENT__TENANT_ID=your-tenant-id-here
- ICP_AGENT_AGENT__NODE_ID={{.Node.Hostname}}
- ICP_AGENT_ICP_SERVICE__ADDRESS=https://dev.api.authsec.dev/spiresvc
volumes:
- agent-socket:/run/spire/sockets
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- icp-network

web-service:
image: my-registry.example.com/web-service:latest
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
restart_policy:
condition: on-failure
environment:
- SPIFFE_ENDPOINT_SOCKET=unix:///run/spire/sockets/agent.sock
volumes:
- agent-socket:/run/spire/sockets:ro
labels:
- "app=web-service"
- "env=production"
networks:
- icp-network

volumes:
agent-socket:

networks:
icp-network:
driver: overlay

Deploy:

docker stack deploy -c docker-compose.yml icp-stack

Monitoring

Prometheus

services:
prometheus:
image: prom/prometheus:latest
command:
- '--config.file=/etc/prometheus/prometheus.yml'
configs:
- source: prometheus-config
target: /etc/prometheus/prometheus.yml
networks:
- icp-network

configs:
prometheus-config:
file: ./prometheus.yml

File: prometheus.yml

scrape_configs:
- job_name: 'icp-agent'
static_configs:
- targets: ['icp-agent:8080']

Log Aggregation

services:
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
networks:
- icp-network

promtail:
image: grafana/promtail:latest
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/log:/var/log:ro
command: -config.file=/etc/promtail/config.yml
networks:
- icp-network

Backup and Recovery

# Backup agent SVID cache
docker exec icp-agent tar czf - /var/lib/icp-agent > agent-backup.tar.gz

# Restore
docker exec -i icp-agent tar xzf - -C / < agent-backup.tar.gz
docker restart icp-agent

Troubleshooting

Issue 1: Agent Can't Access Docker API

Symptoms:

Error: permission denied while connecting to Docker daemon

Solution:

# Check socket mount
docker inspect icp-agent | grep -A 5 "Mounts"

# Fix permissions
sudo chmod 666 /var/run/docker.sock

# Or add agent to docker group (in Dockerfile)
RUN usermod -aG docker icp-agent

Issue 2: Workload Can't Connect to Agent

Symptoms:

Error: connect: no such file or directory

Solution:

# Check volume exists
docker volume ls | grep agent-socket

# Check volume is mounted
docker inspect web-service | grep -A 10 "Mounts"

# Recreate volume
docker volume rm agent-socket
docker compose up -d

Issue 3: Wrong SPIFFE ID Issued

# Check collected selectors (agent logs)
docker compose logs icp-agent | grep "Collected selectors"

# Check container labels
docker inspect web-service --format '{{json .Config.Labels}}' | jq

# Check registered workload entries
curl "${ICP_SERVER_URL}/api/v1/workloads?parent_id=spiffe://${TENANT_ID}/agent/${NODE_ID}"

Best Practices

1. Use Labels for Workload Identification

Always add descriptive labels:

labels:
- "app=web-service"
- "env=production"
- "version=v1.2.3"
- "team=backend"
- "criticality=high"

2. Implement Health Checks

healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

3. Use Resource Limits

deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M

4. Implement Graceful Shutdown

import signal
import sys

def signal_handler(sig, frame):
print('Shutting down gracefully...')
# Cleanup code here
sys.exit(0)

signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

5. Use Secrets Management

services:
web-service:
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password

secrets:
db_password:
file: ./secrets/db_password.txt

Next Steps

Questions? Contact support@authsec.dev