Run Your Own Private CA and ACME Server for mTLS, IoT, and Internal Services

Author: Ganesh Velrajan

Last Updated: Fri, Jan 23, 2026

In the previous article, we discussed what is ACME, how it works and how it helps automate certificate management, so that you could automatically issue certificates to internal services such as infrastructures, workloads, and apps.

In this tutorial, we’ll discuss how to setup and run your own private CA server using BastionXP with ACME support, so that you could automatically issue certificates to internal infrastructures, workloads, and apps.

BastionXP CA supports ACME protocol, meaning you can get rid of manually provisioning certificates for internal use and automate your entire certificate management workflow - creation, signing, distribution and renewal.

Why Use BastionXP Private CA ACME Server?

BastionXP Private CA with ACME server support helps organizations take full ownership of their certificate infrastructure without the complexity traditionally associated with PKI. Instead of relying on public certificate authorities (like Let’s Encrypt) or manual certificate issuance, teams can operate a self-hosted, automated, and policy-driven CA that is purpose-built for internal systems, private networks, and modern cloud environments.

With native ACME protocol support, BastionXP enables seamless automation of certificate issuance, renewal, and rotation across servers, Kubernetes clusters, internal applications, APIs, and IoT devices. This eliminates certificate sprawl, reduces operational risk from expired certificates, and removes the need for fragile scripts or manual workflows. Existing ACME clients such as Certbot, acme.sh, and platform-native integrations can be used without modification, allowing teams to adopt BastionXP with minimal friction.

Security teams benefit from stronger trust boundaries and zero-trust alignment. Certificates issued by BastionXP Private CA are scoped exclusively for internal use, preventing unintended external trust while enabling mTLS, workload identity, and service-to-service authentication. Fine-grained policies, short-lived certificates, and centralized lifecycle control significantly reduce the blast radius of compromised credentials and align with modern security best practices.

For organizations operating in regulated, air-gapped, or hybrid environments, BastionXP removes the dependency on external CAs and internet connectivity. This makes it ideal for enterprises with compliance requirements, on-prem infrastructure, private cloud deployments, or edge environments where public CA usage is impractical or prohibited.

Overall, BastionXP Private CA with ACME server support delivers a modern, automated, and secure private PKI that scales with organizational growth—helping platform, DevOps, and security teams move faster while maintaining strong cryptographic trust and operational control.

How to setup and run BastionXP CA with ACME Server

Download and Install

Follow the instructions here to download and install BastionXP for your OS version.

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"],
            "policy": {
                "x509": {
                    "dns": {
                        "allow": ["*.internal.example.com"],
                        "deny": ["abc.internal.example.com", "xyz.internal.example.com"]
                    },
                    "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"]
                    }
                }
            }
        }
   ]	
}

ACME Base URL

The ACME base URL is: https://ca.internal.example.com/acme

ACME Directory:

The ACME directory URL is: https://ca.internal.example.com/acme/directory

How to get a certificate from BastionXP

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=$(my_app_dir)/certs/root_ca.crt \
		certbot certonly -n --standalone -d db-001.internal.example.com \
		--server https://ca.internal.example.com/acme/directory

Description:

  • sudo is required in certbot’s standalone mode so that it can run a HTTP server on port 80 to complete the http-01 challenge.
  • If you already have a HTTP webserver running, you can use webroot mode instead.
  • With the appropriate plugin certbot also supports the dns-01 challenge 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=$(my_app_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 --server flag
  • Make acme.sh to trust your root certificate using the --ca-bundle flag

For example:

    $ sudo acme.sh --issue --standalone -d db-001.internal.example.com \
        --server https://ca.internal.example.com/acme/directory \
        --ca-bundle $(my_app_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

You can use the Go’s simple ACME client (Autocert) library 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 = 'abc.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.');
});

Conclusion

Deploying an internal Certificate Authority is most effective when certificate lifecycle management can be fully automated and seamlessly integrated into existing infrastructure.

BastionXP Private CA achieves this by providing a robust, ACME-compliant interface that works reliably with a wide range of industry-standard ACME clients, including Certbot, lego, Go autocert, NodeJS acme-client and custom implementations.

This compatibility allows teams to leverage familiar tooling while automating certificate issuance, renewal, and rotation for internal services such as microservices, APIs, ingress controllers, load balancers, and device endpoints.

By supporting multiple ACME challenge types and accommodating the real-world behaviors of different clients, BastionXP CA enables organizations to embed certificate management directly into CI/CD pipelines, infrastructure-as-code workflows, and service bootstrap processes.

Certificates can be issued on demand, renewed automatically, and rotated without downtime—reducing operational overhead and eliminating manual key management.

As internal environments grow more dynamic and security requirements become stricter, BastionXP’s standards-aligned and interoperable ACME implementation provides a scalable foundation for enforcing encrypted communication, service identity, and Zero Trust principles across modern infrastructure.

FAQs

General ACME & Private CA FAQs

  1. What is BastionXP Private CA? BastionXP Private CA is a self-hosted certificate authority designed for issuing and managing TLS certificates for internal systems, private networks, and enterprise infrastructure without relying on public CAs.

  2. What is an ACME server and why does BastionXP support it? An ACME server automates certificate issuance and renewal using a standardized protocol. BastionXP supports ACME to enable seamless, hands-off certificate lifecycle management using existing ACME clients and tooling.

  3. How is BastionXP different from public CAs like Let’s Encrypt? Unlike public CAs, BastionXP issues certificates for internal and private use cases, offers full control over trust boundaries, and does not require public DNS or internet-facing services.

  4. Do I need to replace my existing ACME clients to use BastionXP? No. BastionXP is ACME-compatible and works with standard clients such as Certbot, acme.sh, and platform-native integrations without modification.

Security & Trust Model FAQs

  1. Is BastionXP suitable for zero-trust architectures? Yes. BastionXP enables short-lived certificates, mTLS, and service-to-service authentication, making it well-suited for zero-trust and identity-based security models.

  2. Can BastionXP be used for mutual TLS (mTLS)? Yes. BastionXP can issue client and server certificates for mTLS between services, workloads, APIs, and internal applications.

  3. How does BastionXP protect against certificate compromise? BastionXP supports automated rotation, short certificate lifetimes, centralized revocation, and strict issuance policies to reduce the impact of compromised credentials.

  4. Are certificates issued by BastionXP trusted publicly? No. BastionXP certificates are intended for private trust environments and must be trusted explicitly by internal systems, devices, or applications.

ACME Challenge & Automation FAQs

  1. Which ACME challenge types does BastionXP support? BastionXP supports standard ACME challenges such as HTTP-01, DNS-01, and TLS-ALPN-01, and may also support device-based or internal validation workflows for private environments.

  2. Can BastionXP issue wildcard certificates? Yes. Wildcard certificates can be issued using the DNS-01 challenge.

  3. Does BastionXP work in air-gapped or offline environments? Yes. BastionXP can be deployed in private, on-prem, or air-gapped environments without dependency on external public CAs.

  4. How are certificate renewals handled? Renewals are fully automated via ACME. Clients periodically request renewal, complete validation, and receive updated certificates without manual intervention.

Platform & Integration FAQs

  1. Can BastionXP integrate with Kubernetes? Yes. BastionXP can integrate with Kubernetes using ACME-compatible tools such as cert-manager for automated certificate management.

  2. Can BastionXP be used for internal APIs and microservices? Absolutely. BastionXP is well-suited for securing internal APIs, microservices, and service meshes using TLS and mTLS.

  3. Does BastionXP support IoT or edge devices? Yes. BastionXP can issue certificates to devices for secure device identity, authentication, and encrypted communication in IoT and edge deployments.

Operations & Compliance FAQs

  1. Who should manage BastionXP in an organization? BastionXP is typically managed by platform, DevOps, or security teams responsible for PKI, infrastructure security, or zero-trust initiatives.

  2. Is BastionXP suitable for regulated environments? Yes. BastionXP helps organizations meet compliance requirements by maintaining full control over certificate issuance, trust roots, and auditability.

  3. Can BastionXP replace traditional enterprise PKI solutions? For many modern use cases, yes. BastionXP offers a simpler, more automated alternative to traditional PKI systems while maintaining strong security controls.

  4. How does BastionXP scale as infrastructure grows? BastionXP is designed to scale with automated issuance, stateless ACME workflows, and centralized policy enforcement across large environments.

Getting Started FAQs

  1. How quickly can organizations get started with BastionXP Private CA? Most organizations can deploy BastionXP and begin issuing certificates within minutes using standard ACME clients and existing automation.

  2. Can BastionXP coexist with public CAs? Yes. Many organizations use BastionXP for internal certificates while continuing to use public CAs for external-facing services.

Start Your Free Trial Now

Try BastionXP for free with no commitments. No credit card required.