Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6da767d
pkg/autocert: add ACME certificate manager using lego v4
Sep 4, 2026
ef68289
pkg/autocert: add Revoke and shared RenewDue for renewal checks
Sep 5, 2026
accdec6
controller: add ACME certificate management via Let's Encrypt
Sep 5, 2026
bfd4bd9
cli: add flynn cert letsencrypt command
Sep 5, 2026
19ac47a
cli: add --acme snapshot option to route update
Sep 5, 2026
31aae00
router: add acme_domain field to Route and HTTPRoute
Sep 5, 2026
3ea3415
controller/data: add acme_domain column and ACME cert route sync
Sep 5, 2026
dd8b73d
controller: wire ACME store to sync route certs on renewal
Sep 5, 2026
a48f142
cli: persist acme_domain on route update --acme
Sep 5, 2026
d5c18ff
controller/data: add ACMEStore integration tests
Sep 5, 2026
548975c
build: lower go directive to 1.23 to fix CI coverage tool
Sep 5, 2026
f1fef2f
build: restore go 1.25.0 directive for upgraded dependencies
Sep 5, 2026
0add5b1
ci: install Go 1.25.0 to match go.mod dependencies
Sep 5, 2026
2656118
pkg/autocert: add Pebble ACME integration test for HTTP-01
Sep 5, 2026
0666efe
autocert: fix AutoDNS DNS-01 provider for subdomains
Sep 5, 2026
fb074c8
vendor: fix go-tuf Commit panic on dot-less target names
Sep 6, 2026
05415c9
host: restore cgroup v2 support in vendored runc
Sep 7, 2026
58dd0f1
controller: fix ACME cert route conflict with httprouter wildcards
Sep 7, 2026
221fb87
util: update CA bundle to current Mozilla roots and use Go-default path
Sep 7, 2026
3bbdf90
controller/data: fix ACME route sync event collision
Sep 7, 2026
e9991a2
builder: ship dashboard web assets in image
Sep 7, 2026
2cb6274
feat: add certificate details to dashboard routes view
Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CI workflow for Flynn
#
# Builds all components and runs unit tests on every push and pull request
# to the tuf-rebuild branch. Uses a Debian container with Go 1.22.12 and
# to the tuf-rebuild branch. Uses an Ubuntu container with Go 1.25.0 and
# all required system dependencies.
#
# The build uses script/bootstrap-build which compiles all 34 Flynn binaries
Expand Down Expand Up @@ -35,9 +35,9 @@ jobs:
curl \
ca-certificates

- name: Install Go 1.22.12
- name: Install Go 1.25.0
run: |
curl -fsSL "https://go.dev/dl/go1.22.12.linux-amd64.tar.gz" \
curl -fsSL "https://go.dev/dl/go1.25.0.linux-amd64.tar.gz" \
| tar -C /usr/local -xz
echo "/usr/local/go/bin" >> $GITHUB_PATH
echo "GOROOT=/usr/local/go" >> $GITHUB_ENV
Expand Down Expand Up @@ -78,9 +78,9 @@ jobs:
curl \
ca-certificates

- name: Install Go 1.22.12
- name: Install Go 1.25.0
run: |
curl -fsSL "https://go.dev/dl/go1.22.12.linux-amd64.tar.gz" \
curl -fsSL "https://go.dev/dl/go1.25.0.linux-amd64.tar.gz" \
| tar -C /usr/local -xz
echo "/usr/local/go/bin" >> $GITHUB_PATH
echo "GOROOT=/usr/local/go" >> $GITHUB_ENV
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test-unit-root: test-unit
# Redis, ZFS, etc.) and can run in any Linux environment with Go installed.
TEST_PACKAGES_STANDALONE = \
./pkg/attempt/... \
./pkg/autocert/... \
./pkg/cors/... \
./pkg/ipallocator/... \
./pkg/lru/... \
Expand Down
8 changes: 4 additions & 4 deletions builder/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
"host/flynn-init": "/usr/local/bin/flynn-init"
},
"copy": {
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certs.pem",
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certificates.crt",
"host/zfs-mknod.sh": "/usr/local/bin/zfs-mknod",
"host/udev.rules": "/lib/udev/rules.d/10-local.rules",
"host/start.sh": "/usr/local/bin/start-flynn-host.sh",
Expand Down Expand Up @@ -218,7 +218,7 @@
},
"copy": {
"controller/start.sh": "/bin/start-flynn-controller",
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certs.pem"
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certificates.crt"
},
"inputs": [
"schema/*.json",
Expand Down Expand Up @@ -381,7 +381,7 @@
"blobstore": "/bin/flynn-blobstore"
},
"copy": {
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certs.pem"
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certificates.crt"
},
"limits": {
"temp_disk": "2G"
Expand All @@ -404,7 +404,7 @@
"router": "/bin/flynn-router"
},
"copy": {
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certs.pem"
"util/ca-certs/ca-certs.pem": "/etc/ssl/certs/ca-certificates.crt"
}
}
],
Expand Down
212 changes: 212 additions & 0 deletions cli/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package main

import (
"errors"
"fmt"
"strconv"
"strings"
"time"

controller "github.com/flynn/flynn/controller/client"
ct "github.com/flynn/flynn/controller/types"
"github.com/flynn/go-docopt"
)

func init() {
register("cert", runCert, `
usage: flynn cert
flynn cert letsencrypt [--challenge <challenge>] [--dns-provider <provider>] [--dns-config <key=value>...] <domain>...
flynn cert letsencrypt --status <domain>
flynn cert letsencrypt --revoke <domain>
flynn cert letsencrypt --list
flynn cert letsencrypt --config [--enabled <enabled>] [--email <email>] [--ca-url <url>] [--challenge <challenge>] [--dns-provider <provider>] [--dns-config <key=value>...]

Manage TLS certificates and ACME (Let's Encrypt) configuration.

Options:
--status=<domain> show the certificate status for a domain
--revoke=<domain> revoke the certificate for a domain
--list list certificates managed via Let's Encrypt
--config show or update the ACME configuration
--enabled=<enabled> enable or disable ACME provisioning (true/false)
--email=<email> ACME account contact email
--ca-url=<url> ACME directory URL
--challenge=<challenge> challenge type: http-01 or dns-01 [default: http-01]
--dns-provider=<provider> DNS provider for dns-01 challenges (e.g. autodns)
--dns-config=<key=value> DNS provider configuration (repeatable)

Commands:
With no arguments, shows this help.

Examples:

$ flynn cert letsencrypt example.com

$ flynn cert letsencrypt example.com www.example.com

$ flynn cert letsencrypt example.com --challenge dns-01 --dns-provider autodns --dns-config api_user=user --dns-config api_password=secret

$ flynn cert letsencrypt --list

$ flynn cert letsencrypt --status example.com

$ flynn cert letsencrypt --revoke example.com

$ flynn cert letsencrypt --config --email admin@example.com
`)
}

func runCert(args *docopt.Args, client controller.Client) error {
if !args.Bool["letsencrypt"] {
return errors.New("Usage: flynn cert letsencrypt ...")
}
switch {
case args.Bool["--list"]:
return runACMECertList(client)
case args.String["--status"] != "":
return runACMECertStatus(args, client)
case args.String["--revoke"] != "":
return runACMECertRevoke(args, client)
case args.Bool["--config"]:
return runACMECertConfig(args, client)
default:
return runACMECertIssue(args, client)
}
}

func runACMECertIssue(args *docopt.Args, client controller.Client) error {
domains := args.All["<domain>"].([]string)
if len(domains) == 0 {
return errors.New("at least one domain is required")
}
challenge := args.String["--challenge"]
if challenge == "" {
challenge = "http-01"
}

cert, err := client.ProvisionACMECert(&ct.ACMECertRequest{Domains: domains})
if err != nil {
return err
}
fmt.Printf("Provisioned certificate for %s (expires %s)\n", cert.Domain, cert.ExpiresAt.Format(time.RFC3339))
return nil
}

func runACMECertStatus(args *docopt.Args, client controller.Client) error {
domain := args.String["<domain>"]
cert, err := client.GetACMECert(domain)
if err != nil {
return err
}
printACMECert(cert)
return nil
}

func runACMECertList(client controller.Client) error {
certs, err := client.ACMECertList()
if err != nil {
return err
}
if len(certs) == 0 {
fmt.Println("No certificates managed via Let's Encrypt.")
return nil
}
w := tabWriter()
defer w.Flush()
listRec(w, "DOMAIN", "EXPIRES", "ACCOUNT")
for _, cert := range certs {
listRec(w, cert.Domain, humanTime(&cert.ExpiresAt), cert.AccountEmail)
}
return nil
}

func runACMECertRevoke(args *docopt.Args, client controller.Client) error {
domain := args.String["<domain>"]
if err := client.RevokeACMECert(domain); err != nil {
return err
}
fmt.Printf("Revoked certificate for %s\n", domain)
return nil
}

func runACMECertConfig(args *docopt.Args, client controller.Client) error {
cfg, err := client.GetACMEConfig()
if err != nil {
return err
}

update := false
req := &ct.ACMEConfig{
Enabled: cfg.Enabled,
Email: cfg.Email,
CAURL: cfg.CAURL,
ChallengeType: cfg.ChallengeType,
DNSProvider: cfg.DNSProvider,
DNSConfig: cfg.DNSConfig,
}
if v := args.String["--email"]; v != "" {
req.Email = v
update = true
}
if v := args.String["--ca-url"]; v != "" {
req.CAURL = v
update = true
}
if v := args.String["--challenge"]; v != "" {
req.ChallengeType = v
update = true
}
if v := args.String["--dns-provider"]; v != "" {
req.DNSProvider = v
update = true
}
if v := args.String["--enabled"]; v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return fmt.Errorf("invalid value for --enabled: %q", v)
}
req.Enabled = enabled
update = true
}
if kvs, ok := args.All["--dns-config"].([]string); ok && len(kvs) > 0 {
config := make(map[string]string)
for _, kv := range kvs {
parts := strings.SplitN(kv, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid dns config %q, expected key=value", kv)
}
config[parts[0]] = parts[1]
}
req.DNSConfig = config
update = true
}

if update {
var err error
cfg, err = client.UpdateACMEConfig(req)
if err != nil {
return err
}
}

fmt.Printf("Enabled: %t\n", cfg.Enabled)
fmt.Printf("Email: %s\n", cfg.Email)
if cfg.CAURL != "" {
fmt.Printf("CA URL: %s\n", cfg.CAURL)
}
fmt.Printf("Challenge type: %s\n", cfg.ChallengeType)
if cfg.DNSProvider != "" {
fmt.Printf("DNS provider: %s\n", cfg.DNSProvider)
}
return nil
}

func printACMECert(cert *ct.ACMECert) {
fmt.Printf("ID: %s\n", cert.ID)
fmt.Printf("Domain: %s\n", cert.Domain)
fmt.Printf("Domains: %s\n", strings.Join(cert.Domains, ", "))
fmt.Printf("Account email: %s\n", cert.AccountEmail)
fmt.Printf("Issued: %s\n", cert.CreatedAt.Format(time.RFC3339))
fmt.Printf("Updated: %s\n", cert.UpdatedAt.Format(time.RFC3339))
fmt.Printf("Expires: %s (%s)\n", cert.ExpiresAt.Format(time.RFC3339), humanTime(&cert.ExpiresAt))
}
26 changes: 22 additions & 4 deletions cli/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func init() {
usage: flynn route
flynn route add http [-s <service>] [-p <port>] [-c <tls-cert> -k <tls-key>] [--sticky] [--leader] [--no-leader] [--no-drain-backends] [--disable-keep-alives] <domain>
flynn route add tcp [-s <service>] [-p <port>] [--leader] [--no-drain-backends]
flynn route update <id> [-s <service>] [-c <tls-cert> -k <tls-key>] [--sticky] [--no-sticky] [--leader] [--no-leader] [--disable-keep-alives] [--enable-keep-alives]
flynn route update <id> [-s <service>] [-c <tls-cert> -k <tls-key>] [--acme <domain>] [--sticky] [--no-sticky] [--leader] [--no-leader] [--disable-keep-alives] [--enable-keep-alives]
flynn route remove <id>

Manage routes for application.
Expand All @@ -30,6 +30,7 @@ Options:
-s, --service=<service> service name to route domain to (defaults to APPNAME-web)
-c, --tls-cert=<tls-cert> path to PEM encoded certificate for TLS, - for stdin (http only)
-k, --tls-key=<tls-key> path to PEM encoded private key for TLS, - for stdin (http only)
--acme=<domain> domain of an ACME-issued certificate to attach to the route (update http only; replaces -c/-k)
--sticky enable cookie-based sticky routing (http only)
--no-sticky disable cookie-based sticky routing (update http only)
--leader enable leader-only routing mode
Expand Down Expand Up @@ -224,6 +225,12 @@ func runRouteUpdateTCP(args *docopt.Args, client controller.Client) error {
}

func runRouteUpdateHTTP(args *docopt.Args, client controller.Client) error {
if domain := args.String["--acme"]; domain != "" {
if args.String["--tls-cert"] != "" || args.String["--tls-key"] != "" {
return errors.New("--acme cannot be combined with -c/--tls-cert or -k/--tls-key")
}
}

id := args.String["<id>"]
appName := mustApp()

Expand All @@ -237,9 +244,20 @@ func runRouteUpdateHTTP(args *docopt.Args, client controller.Client) error {
}

route.Certificate = nil
route.LegacyTLSCert, route.LegacyTLSKey, err = parseTLSCert(args)
if err != nil {
return err
if domain := args.String["--acme"]; domain != "" {
cert, err := client.GetACMECert(domain)
if err != nil {
return err
}
route.ACMEDomain = domain
route.LegacyTLSCert = cert.Cert
route.LegacyTLSKey = cert.Key
} else {
route.ACMEDomain = ""
route.LegacyTLSCert, route.LegacyTLSKey, err = parseTLSCert(args)
if err != nil {
return err
}
}

if args.Bool["--sticky"] {
Expand Down
Loading
Loading