# ACME Based Certificate Provisioning
ACME stands for Automatic Certificate Management Environment.
BastionXP can automate host, server, workload and device certificate provisioning using ACME protocol (RFC 8555).
BastionXP ACME server supports the following ACME challenge types: "http-01", "dns-01", "tls-alpn-01".
BastionXP can verify both domain name and IP address based host certificate requests using appropriate ACME challenge types. For example, DNS-01 type challenge is not available to verify IP address based certificate requests.
# Configuration
Enable ACME protocol based certificate provisioner in your BastionXP CA instance using the below sample configuration.
{
"mode": "auth",
"email": "[email protected]",
"gateway_domain": "ca.internal.example.com",
"provisioners": [
{
"type": "ACME",
"name": "acme",
"tos": "https://www.example.com/tos",
"website": "https://www.example.com",
"dns_server": "dns.internal.example.com:1053",
"challenges": ["http-01", "dns-01", "tls-alpn-01"],
"require_eab": true,
"policy": {
"x509": {
"dns": {
"allow": ["*.internal.example.com"],
"deny": ["abc.internal.example.com", "xyz.internal.example.com"],
"allow_wildcards": true,
},
"ip": {
"allow": ["10.1.1.0/24", "20.1.1.4", "30.2.29.125/32"],
"deny": ["2.2.0.0/16", "3.4.5.1"]
}
}
}
}
]
}
BastionXP CA's Policy Engine provides you with a fine-grained control over ACME certificate provisioning using user-defined custom policy configurations. BastionXP CA provides you the option of creating and applying custom policies to the ACME certificate provisioner. You can allow or deny certificates from being issued to certain domains or IP addresses.
BastionXP also implements ACME External Account Binding(EAB). BastionXP restricts who can get certificate from the ACME server by enforcing ACME EAB. ACME clients need to provide a valid EAB credentials to register an account with the server, thereby preventing anonymous ACME clients from registering and obtaining certificates from the CA.
# ACME Directory URL:
The ACME directory URL is: https://ca.internal.example.com/acme/directory
# How to get a Certificate from BastionXP using ACME Clients
# Using Certbot
To register an ACME account with the BastionXP CA using certbot, use the below command:
$ sudo certbot register --agree-tos -m [email protected] \
--server https://ca.internal.example.com/acme/directory
To get a certificate from BastionXP CA using certbot you need to:
- Provide certbot with your ACME directory URL using the --server flag
- Make certbot to trust your CA's root certificate using the REQUESTS_CA_BUNDLE environment variable
For example:
$ sudo REQUESTS_CA_BUNDLE=$(HOME_DIR)/certs/root_ca.crt \
certbot certonly -n --standalone -d db-001.internal.example.com \
--server https://ca.internal.example.com/acme/directory
Description:
sudois required in certbot's standalone mode so that it can run a HTTP server on port 80 to complete thehttp-01challenge.- If you already have a HTTP webserver running, you can use webroot mode instead.
- With the appropriate plugin
certbotalso supports thedns-01challenge for most popular DNS providers such as AWS, GCP, Cloudflare and more.
To renew all your certificates you've installed using cerbot, run the below command:
$ sudo REQUESTS_CA_BUNDLE=$(HOME_DIR)/certs/root_ca.crt \
certbot renew
# Using acme.sh
To get a certificate from BastionXP CA using acme.sh you need to:
- Provide acme.sh with your ACME directory URL using the
--serverflag - Make
acme.shto trust your root certificate using the--ca-bundleflag
For example:
$ sudo acme.sh --issue --standalone -d db-001.internal.example.com \
--server https://ca.internal.example.com/acme/directory \
--ca-bundle $(HOME_DIR)/certs/root_ca.crt \
--fullchain-file db-001.crt \
--key-file db-001.key
acme.sh can solve the http-01 challenge in standalone mode and webroot mode. It can also solve the dns-01 challenge for various DNS providers.
# Using Lego
To get a certificate using Lego you can use the below sample Go code to use the Lego library:
package main
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
)
// You'll need a user or account type that implements acme.User
type MyUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *MyUser) GetEmail() string {
return u.Email
}
func (u MyUser) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
func main() {
// Create a user. New accounts need an email and private key to start.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
myUser := MyUser{
Email: "[email protected]",
key: privateKey,
}
config := lego.NewConfig(&myUser)
config.CADirURL = "https://ca.internal.example.com/acme/directory"
config.Certificate.KeyType = certcrypto.EC256
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
if err != nil {
log.Fatal(err)
}
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80"))
if err != nil {
log.Fatal("HTTP-01 challenge error: ", err)
}
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Fatal(err)
}
myUser.Registration = reg
request := certificate.ObtainRequest{
Domains: []string{"db-001.internal.example.com"}, // Obtain cert for this domain
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Fatal(err)
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. SAVE THESE TO DISK.
fmt.Printf("%#v\n", certificates)
// ... all done.
}
# Using Go Autocert
You can use the Go's simple ACME client library (Autocert) to obtain a certificate for your Go server on-the-fly from BastionXP CA, by using the below sample code:
package main
import (
"crypto/tls"
"log"
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
func main() {
dirCachePath := filepath.Join("/tmp", "acme-cert")
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("db-001.internal.example.com"), //Your domain here
Cache: autocert.DirCache(dirCachePath), //Folder for storing certificates
Client: &acme.Client{DirectoryURL: "https://ca.internal.example.com/acme/directory"},
}
tlsConfig := &tls.Config{
GetCertificate: certManager.GetCertificate,
}
log.Println("ACME HTTP server listening on port: 80")
go http.ListenAndServe(":80", certManager.HTTPHandler(nil))
// Auth server listens on port 443
server := http.Server{
Addr: ":443",
Handler: gin.Default(),
TLSConfig: tlsConfig,
}
defer server.Close()
log.Println("Server listening on port: ", "443")
log.Fatal(server.ListenAndServeTLS("", "")) // force to use tls.Config
}
# Using Node.JS
You can use the NodeJS acme-client library to obtain a certificate for your NodeJS server on-the-fly from BastionXP CA, by using the below sample code:
const acme = require('acme-client');
const express = require('express');
const http = require('http');
const DIRECTORY_URL = 'https://ca.internal.example.com/acme/directory'
const DOMAIN = 'db-001.internal.example.com'
const EMAIL = '[email protected]'
/**
* HTTP server for HTTP-01 challenge
*/
const app = express();
// Global store to hold active challenges
const activeChallenges = new Map();
/**
* 1. The Express Route
* This serves the keyAuthorization to the ACME CA
*/
app.get('/.well-known/acme-challenge/:token', (req, res) => {
const token = req.params.token;
const keyAuth = activeChallenges.get(token);
console.log(`[ACME] Challenge requested. Token: ${token} | Found: ${!!keyAuth}`);
if (!keyAuth) {
return res.status(404).send('Challenge not found');
}
// ACME requires text/plain or no content-type at all
res.set('Content-Type', 'text/plain');
res.send(keyAuth);
});
const httpServer = http.createServer(app).listen(80, () => {
console.log('Challenge server listening on port 80');
});
/**
* 2. The Provisioning Functions
*/
async function myCustomChallengeProvisioner(token, keyAuthorization) {
console.log(`[ACME] Storing challenge for token: ${token}`);
activeChallenges.set(token, keyAuthorization);
}
async function myCustomChallengeRemover(token) {
console.log(`[ACME] Removing challenge for token: ${token}`);
activeChallenges.delete(token);
}
async function runAcmeClient() {
try {
/* 1. Initialize Client */
const client = new acme.Client({
directoryUrl: DIRECTORY_URL, // Private CA URL
accountKey: await acme.crypto.createPrivateKey()
});
/* FIX: Register the account before doing anything else */
await client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:'+EMAIL]
});
/* 2. Create Order */
const order = await client.createOrder({
identifiers: [{ type: 'dns', value: DOMAIN }]
});
/* 3. Handle Authorizations & Challenges */
const authorizations = await client.getAuthorizations(order);
for (const authz of authorizations) {
const challenge = authz.challenges.find(c => c.type === 'http-01'); // or dns-01
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
// ACTION: Provision your challenge here (e.g., write file to web server)
await myCustomChallengeProvisioner(challenge.token, keyAuthorization);
/* Notify CA that challenge is ready */
await client.completeChallenge(challenge);
/* Wait for CA to validate this specific authorization */
await client.waitForValidStatus(authz);
await myCustomChallengeRemover(challenge.token);
}
/* 4. Finalize Order (Transitions from 'ready' to 'processing'/'valid') */
const [key, csr] = await acme.crypto.createCsr({
altNames: [DOMAIN],
});
// Submitting the CSR to the CA
await client.finalizeOrder(order, csr);
/* 5. Wait for Certificate Issuance */
// Private CAs may take time to sign; poll until status is 'valid'
const finalizedOrder = await client.waitForValidStatus(order);
/* 6. Download Certificate */
const certificate = await client.getCertificate(finalizedOrder);
console.log('Certificate issued:\n', certificate);
return { key, certificate };
} catch (err) {
console.error('Error:', err);
} finally {
console.log('Closing HTTP server...');
// This allows the process to exit once the server stops listening
httpServer.close();
}
}
runAcmeClient().then(() => {
console.log('ACME client finished.');
});