certmagicazureblob

package module
v0.0.0-...-c0e998e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2026 License: MIT Imports: 28 Imported by: 0

README

certmagic-azureblob

Azure Blob Storage backend for CertMagic / Caddy. Enables distributed TLS certificate management across a Caddy cluster using Azure Blob Storage for persistence and Azure Blob Leases for distributed locking.

Features

  • Distributed storage — certificates, keys, and ACME account data stored in Azure Blob Storage
  • Distributed locking — Azure Blob Leases coordinate certificate renewals across cluster nodes
  • Crash recovery — leases auto-expire (default 30s), so crashed nodes don't hold locks indefinitely
  • Directory semanticsExists, Stat, Delete, and List correctly handle both individual keys and directory prefixes
  • Caddy module — registers as caddy.storage.azure_blob with full Caddyfile support

Installation

Build a custom Caddy binary with this plugin using xcaddy:

xcaddy build --with github.com/lnr0626/certmagic-azureblob

Configuration

Caddyfile
Using a connection string (account-level access)
{
    storage azure_blob {
        connection_string {env.AZURE_STORAGE_CONNECTION_STRING}
        encryption_key    {env.CADDY_CERT_ENCRYPTION_KEY}
        container          caddy-certs
        prefix             production
        lease_duration     30
        clean_lock_blobs
        create_container   false
    }
}
{
    storage azure_blob {
        container_sas_url {env.CADDY_CERTS_SAS_URL}
        encryption_key    {env.CADDY_CERT_ENCRYPTION_KEY}
        prefix             production
        lease_duration     30
    }
}

The container_sas_url option restricts access to a single container with only the permissions granted by the SAS token. The container name is automatically extracted from the URL path — no separate container directive is needed.

Using a service principal (Azure AD)
{
    storage azure_blob {
        tenant_id      {env.AZURE_TENANT_ID}
        client_id      {env.AZURE_CLIENT_ID}
        client_secret  {env.AZURE_CLIENT_SECRET}
        account_url    https://caddycerts.blob.core.windows.net
        encryption_key {env.CADDY_CERT_ENCRYPTION_KEY}
        container      caddy-certs
        prefix         production
        lease_duration 30
        create_container false
    }
}

Service principal auth uses Azure AD client credentials. The account_url is the full blob service endpoint — this supports sovereign clouds (e.g. .blob.core.chinacloudapi.cn), private endpoints, and Azurite for local testing.

JSON config
{
  "storage": {
    "module": "azure_blob",
    "container_sas_url": "{env.CADDY_CERTS_SAS_URL}",
    "encryption_key": "{env.CADDY_CERT_ENCRYPTION_KEY}",
    "prefix": "production",
    "lease_duration": 30
  }
}
Options
Option Default Description
connection_string (none) Azure Storage account connection string. Mutually exclusive with other auth methods
container_sas_url (none) Container-scoped SAS URL for least-privilege access. Mutually exclusive with other auth methods. Container name is extracted from the URL path automatically
tenant_id (none) Azure AD tenant ID for service principal auth. Requires client_id, client_secret, and account_url
client_id (none) Azure AD application (client) ID for service principal auth
client_secret (none) Azure AD client secret for service principal auth
account_url (none) Azure Blob Storage service URL for service principal auth (e.g. https://myaccount.blob.core.windows.net). Supports sovereign clouds, private endpoints, and Azurite
encryption_key (none) Optional hex-encoded 32-byte AES-256-GCM key for client-side encryption
container caddy-certs Blob container name
prefix (none) Optional path prefix for all blob names within the container
lease_duration 30 Blob lease duration in seconds (15–60). Controls crash recovery time
clean_lock_blobs false Delete lock blobs after releasing the lease. See Lock blob cleanup
create_container true Auto-create the container on startup. Set to false when pre-provisioned by infra tooling (allows tighter RBAC)

How it works

Storage

CertMagic keys (e.g., certificates/acme-v02.api.letsencrypt.org-directory/example.com/example.com.crt) are mapped directly to Azure blob names within the configured container. An optional prefix namespaces all blobs.

Distributed locking

When CertMagic needs to acquire a lock (e.g., for certificate renewal), the plugin:

  1. Creates a lock blob at locks/{name} (idempotent — skips if it already exists)
  2. Acquires an Azure Blob Lease on the lock blob
  3. Starts a background goroutine that renews the lease at 2/3 of the lease duration
  4. On unlock, cancels the renewal goroutine and releases the lease

If a Caddy instance crashes, the lease expires automatically after lease_duration seconds, allowing another instance to acquire the lock.

Lease duration tradeoffs
Duration Crash recovery time Renewal safety margin
15s Fast (15s) Tight (5s between renewals)
30s (default) Moderate (30s) Comfortable (10s margin)
60s Slow (60s) Very safe (20s margin)
Lock blob cleanup

The plugin creates empty blobs at locks/{name} to serve as lease targets. By default, these blobs are not deleted after the lease is released, which means they accumulate over time.

Two cleanup strategies are available:

Option 1: clean_lock_blobs (simple)

Set clean_lock_blobs in your Caddyfile or JSON config. The plugin will delete each lock blob immediately after releasing the lease. This adds one extra API call per unlock but keeps the container tidy.

Note: If a Caddy instance crashes before unlocking, the lock blob will remain. This is harmless — the lease expires automatically and the blob will be cleaned up on the next successful lock/unlock cycle for that name.

Use an Azure lifecycle management policy to automatically delete old lock blobs. This handles crash-orphaned blobs without any plugin-side logic:

az storage account management-policy create \
  --account-name caddycerts \
  --resource-group mygroup \
  --policy '{
    "rules": [{
      "enabled": true,
      "name": "cleanup-lock-blobs",
      "type": "Lifecycle",
      "definition": {
        "filters": {
          "blobTypes": ["blockBlob"],
          "prefixMatch": ["caddy-certs/locks/"]
        },
        "actions": {
          "baseBlob": {
            "delete": {
              "daysAfterModificationGreaterThan": 30
            }
          }
        }
      }
    }]
  }'

Adjust prefixMatch to include your container name and any configured prefix (e.g., caddy-certs/production/locks/).

Azure setup

Create a storage account
az storage account create \
  --name caddycerts \
  --resource-group mygroup \
  --location eastus \
  --sku Standard_LRS

# Get the connection string
az storage account show-connection-string \
  --name caddycerts \
  --resource-group mygroup \
  --output tsv
Permissions
Connection string

The connection string provides account-level access. If create_container is true (the default), the identity also needs permission to create containers. For tighter RBAC, pre-create the container and set create_container false.

A container SAS URL restricts access to a single container. Required SAS permissions:

  • Read (r) — load certificates, keys, and lock blobs
  • Write (w) — store certificates/keys, acquire/renew blob leases
  • Delete (d) — delete certificates/keys, break blob leases
  • List (l) — list blobs for directory semantics

Generate a SAS with a stored access policy for revocability:

# Create a stored access policy
az storage container policy create \
  --container-name caddy-certs \
  --account-name caddycerts \
  --name caddy-rw \
  --permissions rwdl \
  --expiry "$(date -u -v+2y '+%Y-%m-%dT%H:%M:%SZ')"

# Generate a SAS URL from the policy
ACCOUNT="caddycerts"
CONTAINER="caddy-certs"
SAS=$(az storage container generate-sas \
  --account-name "$ACCOUNT" \
  --name "$CONTAINER" \
  --policy-name caddy-rw \
  --https-only \
  -o tsv)
echo "https://${ACCOUNT}.blob.core.windows.net/${CONTAINER}?${SAS}"

Pre-create the container since SAS tokens can't create containers:

az storage container create \
  --name caddy-certs \
  --account-name caddycerts \
  --auth-mode login
Service principal

Create an Azure AD app registration and assign the Storage Blob Data Contributor role on the storage account (or container, for tighter scoping):

# Create the app registration and service principal
az ad app create --display-name caddy-certs-sp
APP_ID=$(az ad app list --display-name caddy-certs-sp --query '[0].appId' -o tsv)
az ad sp create --id "$APP_ID"

# Create a client secret
az ad app credential reset --id "$APP_ID" --query password -o tsv

# Assign Storage Blob Data Contributor on the storage account
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
az role assignment create \
  --assignee "$APP_ID" \
  --role "Storage Blob Data Contributor" \
  --scope "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/mygroup/providers/Microsoft.Storage/storageAccounts/caddycerts"

If create_container is true, the principal also needs container-create permission (included in Storage Blob Data Contributor). For tighter RBAC, pre-create the container and set create_container false.

Testing

Unit tests
go test -v ./...
Integration tests (requires Azurite)
# Start Azurite
docker run -d -p 10000:10000 mcr.microsoft.com/azure-storage/azurite azurite-blob --blobHost 0.0.0.0

# Run integration tests
go test -tags integration -v ./...

Or with a real Azure Storage account:

AZURE_STORAGE_CONNECTION_STRING="..." go test -tags integration -v ./...

License

MIT

Documentation

Overview

Package certmagicazureblob provides an Azure Blob Storage backend for CertMagic, enabling distributed TLS certificate management across a Caddy cluster.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AzureBlobStorage

type AzureBlobStorage struct {

	// ConnectionString is the Azure Storage account connection string.
	// Provides account-level access. Mutually exclusive with other auth methods.
	ConnectionString string `json:"connection_string,omitempty"`

	// ContainerSASURL is a container-scoped SAS URL for least-privilege access.
	// Restricts the plugin to a single container with only the permissions
	// granted by the SAS token. Mutually exclusive with other auth methods.
	// Example: https://<account>.blob.core.windows.net/<container>?<sas-params>
	ContainerSASURL string `json:"container_sas_url,omitempty"`

	// TenantID is the Azure AD tenant ID for service principal auth.
	TenantID string `json:"tenant_id,omitempty"`

	// ClientID is the Azure AD application (client) ID for service principal auth.
	ClientID string `json:"client_id,omitempty"`

	// ClientSecret is the Azure AD client secret for service principal auth.
	ClientSecret string `json:"client_secret,omitempty"`

	// AccountURL is the Azure Blob Storage service URL for service principal auth.
	// Example: https://<account>.blob.core.windows.net
	// Supports sovereign clouds, private endpoints, and Azurite.
	AccountURL string `json:"account_url,omitempty"`

	// EncryptionKey is an optional hex-encoded 32-byte AES-256-GCM key.
	EncryptionKey string `json:"encryption_key,omitempty"`

	// Container is the blob container name. Default: "caddy-certs".
	Container string `json:"container,omitempty"`

	// Prefix is an optional path prefix for all blob names within the container.
	Prefix string `json:"prefix,omitempty"`

	// LeaseDuration is the blob lease duration in seconds (15-60). Default: 30.
	LeaseDuration int32 `json:"lease_duration,omitempty"`

	// CleanLockBlobs deletes lock blobs after releasing the lease. Default: false.
	// When false, empty lock blobs accumulate in the locks/ prefix over time.
	// For most deployments, an Azure Blob lifecycle management policy is the
	// better cleanup approach — see README for details.
	CleanLockBlobs bool `json:"clean_lock_blobs,omitempty"`

	// CreateContainer controls whether the plugin auto-creates the blob container
	// on startup. Default: true. Set to false when the container is pre-provisioned
	// by infrastructure tooling, which allows tighter RBAC (no container-create
	// permission needed).
	CreateContainer *bool `json:"create_container,omitempty"`
	// contains filtered or unexported fields
}

AzureBlobStorage implements certmagic.Storage using Azure Blob Storage. It supports distributed locking via Azure Blob Leases, making it suitable for Caddy clusters that share TLS certificates.

func (*AzureBlobStorage) CaddyModule

func (*AzureBlobStorage) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*AzureBlobStorage) CertMagicStorage

func (s *AzureBlobStorage) CertMagicStorage() (certmagic.Storage, error)

CertMagicStorage returns the storage implementation. Required by Caddy's storage module interface.

func (*AzureBlobStorage) Cleanup

func (s *AzureBlobStorage) Cleanup() error

Cleanup releases all held locks and cleans up resources. Called by Caddy during shutdown.

func (*AzureBlobStorage) Delete

func (s *AzureBlobStorage) Delete(ctx context.Context, key string) error

Delete removes the value at key. If the key is a prefix (directory), all keys under it are deleted. Delete is idempotent — deleting a non-existent key is not an error.

func (*AzureBlobStorage) Exists

func (s *AzureBlobStorage) Exists(ctx context.Context, key string) bool

Exists returns true if the key exists as either a blob or a prefix (directory).

func (*AzureBlobStorage) List

func (s *AzureBlobStorage) List(ctx context.Context, pathPrefix string, recursive bool) ([]string, error)

List returns all keys matching the given path prefix.

func (*AzureBlobStorage) Load

func (s *AzureBlobStorage) Load(ctx context.Context, key string) ([]byte, error)

Load retrieves the value at key.

func (*AzureBlobStorage) Lock

func (s *AzureBlobStorage) Lock(ctx context.Context, name string) error

Lock acquires a distributed lock for the given name. It blocks until the lock is acquired or the context is cancelled. The lock is maintained by a background goroutine that renews the underlying blob lease periodically.

func (*AzureBlobStorage) Provision

func (s *AzureBlobStorage) Provision(ctx caddy.Context) error

Provision sets up the storage module. Called by Caddy after configuration is loaded.

func (*AzureBlobStorage) Stat

Stat returns information about the key. For prefix (directory) keys, it returns a non-terminal KeyInfo.

func (*AzureBlobStorage) Store

func (s *AzureBlobStorage) Store(ctx context.Context, key string, value []byte) error

Store puts value at key. It creates or overwrites the blob.

func (*AzureBlobStorage) Unlock

func (s *AzureBlobStorage) Unlock(ctx context.Context, name string) error

Unlock releases a previously acquired lock.

func (*AzureBlobStorage) UnmarshalCaddyfile

func (s *AzureBlobStorage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile parses the Caddyfile configuration for this module.

storage azure_blob {
    connection_string  "{$AZURE_STORAGE_CONNECTION_STRING}"
    container_sas_url  "{$CADDY_CERTS_SAS_URL}"
    tenant_id          "{$AZURE_TENANT_ID}"
    client_id          "{$AZURE_CLIENT_ID}"
    client_secret      "{$AZURE_CLIENT_SECRET}"
    account_url        "https://myaccount.blob.core.windows.net"
    encryption_key     "{env.CADDY_CERT_ENCRYPTION_KEY}"
    container          caddy-certs
    prefix             ""
    lease_duration     30
    clean_lock_blobs
    create_container   false
}

func (*AzureBlobStorage) Validate

func (s *AzureBlobStorage) Validate() error

Validate checks that the configuration is valid and Azure is reachable. Called by Caddy after Provision, before the module is used.

type ClientProvider

type ClientProvider interface {
	// ContainerClient returns an Azure Blob container client for the given container name.
	// Implementations should cache or reuse clients where appropriate.
	ContainerClient(ctx context.Context, containerName string) (*container.Client, error)
}

ClientProvider abstracts Azure Blob Storage client creation. Implement this interface to support different authentication methods (connection string, managed identity, SAS tokens, etc.).

type ConnStringProvider

type ConnStringProvider struct {
	ConnectionString string
	// contains filtered or unexported fields
}

ConnStringProvider implements ClientProvider using an Azure Storage connection string.

func NewConnStringProvider

func NewConnStringProvider(connectionString string, skipEnsure bool) *ConnStringProvider

NewConnStringProvider creates a ConnStringProvider with the given connection string.

func (*ConnStringProvider) ContainerClient

func (b *ConnStringProvider) ContainerClient(ctx context.Context, containerName string) (*container.Client, error)

type SASClientProvider

type SASClientProvider struct {
	// ContainerSASURL is the full SAS URL for the container, e.g.:
	// https://<account>.blob.core.windows.net/<container>?<sas-params>
	ContainerSASURL string

	// ContainerName is the container name extracted from the SAS URL during provisioning.
	ContainerName string
	// contains filtered or unexported fields
}

SASClientProvider implements ClientProvider using a container-scoped SAS URL. This restricts access to a single container with only the permissions granted by the SAS token — no account-level access, no other containers.

func (*SASClientProvider) ContainerClient

func (p *SASClientProvider) ContainerClient(ctx context.Context, containerName string) (*container.Client, error)

type ServicePrincipalProvider

type ServicePrincipalProvider struct {

	// AccountURL is the Azure Blob Storage service URL, e.g.:
	// https://<account>.blob.core.windows.net
	// Supports sovereign clouds, private endpoints, and Azurite.
	AccountURL string

	TenantID     string
	ClientID     string
	ClientSecret string
	// contains filtered or unexported fields
}

ServicePrincipalProvider implements ClientProvider using Azure AD service principal credentials (client ID, client secret, tenant ID). This is the recommended auth method for production deployments where managed identity is not available and you want to avoid long-lived connection strings.

func NewServicePrincipalProvider

func NewServicePrincipalProvider(accountURL, tenantID, clientID, clientSecret string, skipEnsure bool) *ServicePrincipalProvider

NewServicePrincipalProvider creates a ServicePrincipalProvider with the given credentials.

func (*ServicePrincipalProvider) ContainerClient

func (b *ServicePrincipalProvider) ContainerClient(ctx context.Context, containerName string) (*container.Client, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL