Install#

There are a few methods for produciton installation. The install script is recommended for most deployments due to it’s simplicity.

MethodBest for
Install scriptStandard Linux servers, systemd environments
Build from sourceDevelopment, or architectures not covered by releases
DockerStandalone container-based deployments

The install script downloads the appropriate release binary, creates a dedicated service user, installs a ca.json template, and registers a hardened systemd service unit.

curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh | sudo sh

Environment variable overrides#

All defaults are overridable:

# Defaults
INSTALL_DIR=/opt/acme-proxy
DB_DIR=/opt/acme-proxy/db
CONFIG_FILE=/opt/acme-proxy/ca.json
SERVICE_USER=acme-proxy
SERVICE_GROUP=acme-proxy

Example — custom paths and user:

curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh | \
  sudo INSTALL_DIR=/usr/local/acme-proxy SERVICE_USER=acmeservice sh

What the script installs#

PathDescription
$INSTALL_DIR/step-caThe server binary
$INSTALL_DIR/ca.jsonConfiguration file (template — must be edited)
$DB_DIR/bboltbbolt KV store for ACME account state
/etc/systemd/system/acme-proxy.serviceSystemd service unit

The service is enabled but not started. Configure ca.json file before starting.


Build from Source#

Requirements: Go >= 1.25, libpcsclite-dev (Debian/Ubuntu) or pcsc-lite-devel (RHEL/Rocky)

# Install build dependency
sudo apt-get install -y libpcsclite-dev pkg-config   # Debian / Ubuntu
sudo dnf install -y pcsc-lite-devel pkgconfig        # RHEL / Rocky

# Clone and build
git clone https://github.com/esnet/acme-proxy.git
cd acme-proxy
make

The build produces a step-ca binary in the current directory. Copy it to your install location:

# configure `ca.json` file before starting step-ca
./step-ca ca.json

Use the installer script as a reference to complete the setup with systemd service unit, service account user, permissions etc.


Install using Docker#

Run the following commands before starting the container on a linux host

mkdir -p /opt/acme-proxy/db
touch /opt/acme-proxy/ca.json
chown -R 65532:65532 /opt/acme-proxy

Make sure you have configured the ca.json file

docker run -d \
  --name acme-proxy \
  -p 443:443 \
  -v "$(pwd)"/ca.json:/opt/acme-proxy/ca.json:ro \
  -v "$(pwd)"/db:/opt/acme-proxy/db \
  --restart unless-stopped \
ghcr.io/esnet/acme-proxy:latest

View logs:

docker logs -f acme-proxy

Docker Compose#

services:
  acme-proxy:
    image: ghcr.io/esnet/acme-proxy:latest
    ports:
      - "443:443"
    volumes:
      - ./ca.json:/opt/acme-proxy/ca.json:ro
      - ./db:/opt/acme-proxy/db
    restart: unless-stopped

Starting the Service#

Systemd#

sudo systemctl start acme-proxy
sudo systemctl status acme-proxy

On first start, acme-proxy registers an account with the upstream CA and obtains a TLS certificate for itself. This takes a few seconds. Follow the logs:

sudo journalctl -u acme-proxy -f

Expected startup sequence:

Building new tls configuration using step-ca x509 Signer Interface
Initializing ACME client...
[INFO] acme: Registering account for certadmin@example.com
[INFO] [acmeproxy.example.com] acme: Obtaining bundled SAN certificate
[INFO] [acmeproxy.example.com] acme: Validations succeeded; requesting certificates
Successfully obtained certificate from external CA
Serving HTTPS on :443 ...

Running manually (without systemd)#

/opt/acme-proxy/step-ca /opt/acme-proxy/ca.json

Docker#

docker compose up -d
docker logs -f acme-proxy

Verify#

curl -s https://acmeproxy.example.com/acme/acme/directory | jq .

{
  "newNonce": "https://proxy.example.com/acme/acme/new-nonce",
  "newAccount": "https://proxy.example.com/acme/acme/new-account",
  "newOrder": "https://proxy.example.com/acme/acme/new-order",
  "revokeCert": "https://proxy.example.com/acme/acme/revoke-cert",
  "keyChange": "https://proxy.example.com/acme/acme/key-change"
}

A JSON object with newNonce, newAccount, newOrder keys confirms the ACME server is running and accepting requests.


Verifying Release Binaries#

Each release binary is accompanied by a SHA256 checksum file and a cosign signature bundle for supply-chain verification.

Checksum#

VERSION=v1.0.0   # replace with the release version

curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/step-ca_linux_amd64"
curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/SHA256SUMS"

sha256sum --check --ignore-missing SHA256SUMS

Signature#

Binaries are signed using keyless signing via GitHub Actions OIDC — no long-lived signing key exists. Verification requires the .bundle file published alongside each binary.

# Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/
VERSION=v1.0.0   # replace with the release version

curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/step-ca_linux_amd64"
curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/step-ca_linux_amd64.bundle"

cosign verify-blob \
  --bundle step-ca_linux_amd64.bundle \
  --certificate-identity "https://github.com/esnet/acme-proxy/.github/workflows/ci.yml@refs/tags/${VERSION}" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  step-ca_linux_amd64

A successful verification prints:

Verified OK

Substitute amd64 with arm64 for the ARM binary.