TLS & ACME

Native TLS 1.3 serving path with three cipher suites, automatic Let's Encrypt certificates, SNI-based certificate selection, hot-reload, and TLS 1.2 compatibility fallback.

TLS Overview

ALOS ships a native TLS 1.3 serving path built from scratch. The handshake, record layer, and cryptographic operations used on that path are custom-written in Go with no dependency on crypto/tls for TLS 1.3 serving. Older TLS 1.2/1.1 clients can still fall back through Go's compatibility path when needed.

  • Full TLS 1.3 handshake (ClientHello, ServerHello, EncryptedExtensions, Certificate, Finished)
  • ALPN negotiation for h2 and http/1.1
  • X25519 key exchange
  • ECDSA P-256 certificate verification
  • Transcript hash validation
  • Record-level encryption with sequence-based nonces
  • TLS 1.2/1.1 compatibility fallback for older clients

Cipher Suites

Three TLS 1.3 cipher suites are supported, negotiated automatically based on client preference:

TLS_AES_128_GCM_SHA256

Suite ID: 0x1301
AES-128-GCM encryption
SHA-256 hashing
16-byte key, 12-byte IV

TLS_AES_256_GCM_SHA384

Suite ID: 0x1302
AES-256-GCM encryption
SHA-384 hashing
32-byte key, 12-byte IV

TLS_CHACHA20_POLY1305_SHA256

Suite ID: 0x1303
ChaCha20-Poly1305
SHA-256 hashing
32-byte key, 12-byte IV

Manual Certificates

Load certificates from PEM files:

go
s := core.New(core.Config{
    Addr:        ":443",
    TLSCertFile: "/etc/ssl/cert.pem",
    TLSKeyFile:  "/etc/ssl/key.pem",
})

s.AddCertFromFiles("example.com", "cert.pem", "key.pem")

s.AddCert("example.com", certPEM, keyPEM)

Self-Signed Certificates

ALOS auto-generates self-signed certs for development when no certificate is configured:

go
s := core.New(core.Config{Addr: ":443"})
s.ListenAndServeTLS()

s.AddSelfSignedCert("dev.local")

Self-signed certs are saved to .alos-cert.pem and .alos-key.pem and reused on subsequent runs. They use P-256 ECDSA keys with 365-day validity.

Multi-Domain / SNI

Serve different certificates for different domains using Server Name Indication (SNI):

go
s := core.New(core.Config{
    Addr: ":443",
    Certs: []core.CertConfig{
        {Domain: "example.com",  CertFile: "example.pem",  KeyFile: "example-key.pem"},
        {Domain: "api.example.com", CertFile: "api.pem", KeyFile: "api-key.pem"},
    },
    DefaultDomain: "example.com",
})

s.AddCertFromFiles("new.example.com", "new-cert.pem", "new-key.pem")
s.SetDefaultCert("example.com")
s.RemoveCert("old.example.com")

List Certificates

go
certs := s.ListCerts()
for _, c := range certs {
    fmt.Printf("Domain: %s, Source: %d\n", c.Domain, c.Source)
}

ACME / Let's Encrypt

Automatic certificate issuance and renewal via the ACME protocol (Let's Encrypt):

go
s := core.New(core.Config{
    Addr:     ":443",
    HTTPAddr: ":80",
    ACME: &core.ACMEConfig{
        Email:    "admin@example.com",
        Domains:  []string{"example.com", "www.example.com"},
        CacheDir: ".alos-certs",
    },
})

ACMENode — Challenge Proxy

In a multi-node deployment, set ACMENode to route all HTTP-01 challenge traffic to a dedicated node. Port 80 will proxy /.well-known/acme-challenge/ requests to the specified address instead of handling them locally:

go
s := core.New(core.Config{
    Addr:     ":443",
    HTTPAddr: ":80",
    ACME: &core.ACMEConfig{
        Email:    "admin@example.com",
        Domains:  []string{"example.com"},
        CacheDir: ".alos-certs",
        ACMENode: "10.0.0.5:80",
    },
})

When ACMENode is empty (the default), challenges are handled on the local machine. When set, every ACME challenge request arriving on port 80 is proxied to the configured address.

Field Type Description
Email string Contact email for Let's Encrypt account
CacheDir string Directory for cached certs (default: /etc/letsencrypt)
Domains []string Domains to obtain certs for
ACMENode string Dedicated node address for ACME challenge proxying (e.g. 10.0.0.5:80)

How It Works

  1. On startup, ALOS checks for existing certificates in CacheDir
  2. If missing or expired, it contacts Let's Encrypt to obtain new certificates
  3. HTTP-01 challenges are served automatically on port 80
  4. Certificates are stored to disk for persistence across restarts
  5. A renewal loop runs every 6 hours, renewing 30 days before expiry
  6. DNS validation ensures your domain resolves to this server's IP

Requirements for ACME:

  • Port 80 must be open and reachable from the internet
  • Domain(s) must resolve to this server's public IP
  • Not behind a NAT without port forwarding

Certificate Management

Full runtime certificate management API:

Method Description
AddCert(domain, certPEM, keyPEM) Add certificate from PEM bytes
AddCerts(domain, certPEM, keyPEM) Alias for AddCert
AddCertFromFiles(domain, certFile, keyFile) Add from PEM files
AddSelfSignedCert(domain) Generate and add self-signed cert
UpdateCert(domain, certPEM, keyPEM) Hot-replace a certificate from PEM bytes
ReloadCert(domain, certFile, keyFile) Hot-replace a certificate from PEM files
RemoveCert(domain) Remove a certificate
SetDefaultCert(domain) Set fallback cert for unknown SNI
ListCerts() List all loaded certificates

Certificate Sources

Source Description
CertSelfSigned Auto-generated on startup
CertManual Loaded from PEM files
CertACME Obtained via Let's Encrypt

Hot Reload

Replace certificates at runtime without restarting the server. Uses lock-free atomic snapshots for zero-downtime updates:

go
err := s.ReloadCert("example.com", "new-cert.pem", "new-key.pem")

Or update directly from PEM bytes you loaded yourself:

go
certPEM, _ := os.ReadFile("renewed-cert.pem")
keyPEM,  _ := os.ReadFile("renewed-key.pem")

err := s.UpdateCert("example.com", certPEM, keyPEM)

Both methods atomically swap the certificate in the CertStore and rebuild the fallback TLS config. Existing connections continue with their original cert; new connections pick up the replacement immediately.

HTTP → HTTPS Redirect

Automatically redirect all HTTP traffic to HTTPS. The HTTP listener also serves ACME challenges:

go
s := core.New(core.Config{
    Addr:     ":443",
    HTTPAddr: ":80",
})

When HTTPAddr is set, port 80 serves ACME HTTP-01 challenges at /.well-known/acme-challenge/ and redirects all other traffic to HTTPS with a 301 status.

Runtime ACME Domains

Add domains dynamically after the server starts. Certificates are obtained automatically:

go
s.AddACMEDomain("shop.example.com")

s.AddACMEDomain("blog.example.com", "admin@blog.example.com")

Or enable ACME after server creation:

go
s := core.New(core.Config{
    Addr:     ":443",
    HTTPAddr: ":80",
})

s.EnableACME(core.ACMEConfig{
    Email:    "admin@example.com",
    Domains:  []string{"example.com"},
    CacheDir: ".alos-certs",
})
Method Description
EnableACME(cfg ACMEConfig) Enable ACME with full configuration
AddACMEDomain(domain string, email ...string) Add a single domain for ACME cert issuance
AddDomainCert(domain string, certPEM, keyPEM []byte) error Manually add a cert for a domain (used internally by ACME)

Production Setup

A complete production TLS server with ACME, manual fallback certs, and HTTP redirect:

go
s := core.New(core.Config{
    Addr:     ":443",
    HTTPAddr: ":80",
    ACME: &core.ACMEConfig{
        Email:    "admin@example.com",
        Domains:  []string{"example.com", "www.example.com"},
        CacheDir: "/etc/alos/certs",
    },
    HandshakeTimeout: 10 * time.Second,
})

s.Router.Use(core.Recovery(), core.Logger())

s.Router.GET("/", func(req *core.Request, resp *core.Response) {
    resp.String("Secure!")
})

s.Router.Build()
log.Fatal(s.ListenAndServeTLS())

Multi-Node with ACMENode

Route all ACME challenges to a single dedicated node in a cluster:

go
s := core.New(core.Config{
    Addr:     ":443",
    HTTPAddr: ":80",
    ACME: &core.ACMEConfig{
        Email:    "admin@example.com",
        Domains:  []string{"example.com"},
        CacheDir: "/etc/alos/certs",
        ACMENode: "10.0.0.5:80",
    },
})

Mix ACME with manually loaded certs for internal domains:

go
s.AddCertFromFiles("internal.corp", "internal.pem", "internal-key.pem")

s.AddSelfSignedCert("dev.internal.corp")

certPEM, _ := os.ReadFile("renewed-cert.pem")
keyPEM,  _ := os.ReadFile("renewed-key.pem")
s.UpdateCert("example.com", certPEM, keyPEM)

certs := s.ListCerts()
for _, c := range certs {
    fmt.Printf("Domain: %s, Source: %d\n", c.Domain, c.Source)
}
ESC