feature/featuretags, all: add ts_omit_acme to disable TLS cert support

I'd started to do this in the earlier ts_omit_server PR but
decided to split it into this separate PR.

Updates #17128

Change-Id: Ief8823a78d1f7bbb79e64a5cab30a7d0a5d6ff4b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-16 10:07:50 -07:00
committed by Brad Fitzpatrick
parent 99b3f69126
commit e180fc267b
19 changed files with 342 additions and 236 deletions
-56
View File
@@ -4,9 +4,7 @@
package ipnlocal
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
@@ -54,9 +52,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
req("POST /logtail/flush"): handleC2NLogtailFlush,
req("POST /sockstats"): handleC2NSockStats,
// Check TLS certificate status.
req("GET /tls-cert-status"): handleC2NTLSCertStatus,
// SSH
req("/ssh/usernames"): handleC2NSSHUsernames,
@@ -497,54 +492,3 @@ func regularFileExists(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsRegular()
}
// handleC2NTLSCertStatus returns info about the last TLS certificate issued for the
// provided domain. This can be called by the controlplane to clean up DNS TXT
// records when they're no longer needed by LetsEncrypt.
//
// It does not kick off a cert fetch or async refresh. It only reports anything
// that's already sitting on disk, and only reports metadata about the public
// cert (stuff that'd be the in CT logs anyway).
func handleC2NTLSCertStatus(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
cs, err := b.getCertStore()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
domain := r.FormValue("domain")
if domain == "" {
http.Error(w, "no 'domain'", http.StatusBadRequest)
return
}
ret := &tailcfg.C2NTLSCertInfo{}
pair, err := getCertPEMCached(cs, domain, b.clock.Now())
ret.Valid = err == nil
if err != nil {
ret.Error = err.Error()
if errors.Is(err, errCertExpired) {
ret.Expired = true
} else if errors.Is(err, ipn.ErrStateNotExist) {
ret.Missing = true
ret.Error = "no certificate"
}
} else {
block, _ := pem.Decode(pair.CertPEM)
if block == nil {
ret.Error = "invalid PEM"
ret.Valid = false
} else {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
ret.Error = fmt.Sprintf("invalid certificate: %v", err)
ret.Valid = false
} else {
ret.NotBefore = cert.NotBefore.UTC().Format(time.RFC3339)
ret.NotAfter = cert.NotAfter.UTC().Format(time.RFC3339)
}
}
}
writeJSON(w, ret)
}
+58 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
//go:build !js && !ts_omit_acme
package ipnlocal
@@ -24,6 +24,7 @@ import (
"log"
randv2 "math/rand/v2"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
@@ -40,6 +41,7 @@ import (
"tailscale.com/ipn/store"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/bakedroots"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/acme"
"tailscale.com/types/logger"
"tailscale.com/util/testenv"
@@ -47,6 +49,10 @@ import (
"tailscale.com/version/distro"
)
func init() {
RegisterC2N("GET /tls-cert-status", handleC2NTLSCertStatus)
}
// Process-wide cache. (A new *Handler is created per connection,
// effectively per request)
var (
@@ -836,3 +842,54 @@ func checkCertDomain(st *ipnstate.Status, domain string) error {
}
return fmt.Errorf("invalid domain %q; must be one of %q", domain, st.CertDomains)
}
// handleC2NTLSCertStatus returns info about the last TLS certificate issued for the
// provided domain. This can be called by the controlplane to clean up DNS TXT
// records when they're no longer needed by LetsEncrypt.
//
// It does not kick off a cert fetch or async refresh. It only reports anything
// that's already sitting on disk, and only reports metadata about the public
// cert (stuff that'd be the in CT logs anyway).
func handleC2NTLSCertStatus(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
cs, err := b.getCertStore()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
domain := r.FormValue("domain")
if domain == "" {
http.Error(w, "no 'domain'", http.StatusBadRequest)
return
}
ret := &tailcfg.C2NTLSCertInfo{}
pair, err := getCertPEMCached(cs, domain, b.clock.Now())
ret.Valid = err == nil
if err != nil {
ret.Error = err.Error()
if errors.Is(err, errCertExpired) {
ret.Expired = true
} else if errors.Is(err, ipn.ErrStateNotExist) {
ret.Missing = true
ret.Error = "no certificate"
}
} else {
block, _ := pem.Decode(pair.CertPEM)
if block == nil {
ret.Error = "invalid PEM"
ret.Valid = false
} else {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
ret.Error = fmt.Sprintf("invalid certificate: %v", err)
ret.Valid = false
} else {
ret.NotBefore = cert.NotBefore.UTC().Format(time.RFC3339)
ret.NotAfter = cert.NotAfter.UTC().Format(time.RFC3339)
}
}
}
writeJSON(w, ret)
}
@@ -1,20 +1,30 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build js || ts_omit_acme
package ipnlocal
import (
"context"
"errors"
"io"
"net/http"
"time"
)
func init() {
RegisterC2N("GET /tls-cert-status", handleC2NTLSCertStatusDisabled)
}
var errNoCerts = errors.New("cert support not compiled in this build")
type TLSCertKeyPair struct {
CertPEM, KeyPEM []byte
}
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
return nil, errors.New("not implemented for js/wasm")
return nil, errNoCerts
}
var errCertExpired = errors.New("cert expired")
@@ -22,9 +32,14 @@ var errCertExpired = errors.New("cert expired")
type certStore interface{}
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
return nil, errors.New("not implemented for js/wasm")
return nil, errNoCerts
}
func (b *LocalBackend) getCertStore() (certStore, error) {
return nil, errors.New("not implemented for js/wasm")
return nil, errNoCerts
}
func handleC2NTLSCertStatusDisabled(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"Missing":true}`) // a minimal tailcfg.C2NTLSCertInfo
}
+5 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android && !js
//go:build !ios && !android && !js && !ts_omit_acme
package localapi
@@ -14,6 +14,10 @@ import (
"tailscale.com/ipn/ipnlocal"
)
func init() {
Register("cert/", (*Handler).serveCert)
}
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite && !h.PermitCert {
http.Error(w, "cert access denied", http.StatusForbidden)
-1
View File
@@ -67,7 +67,6 @@ type LocalAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
// then it's a prefix match.
var handler = map[string]LocalAPIHandler{
// The prefix match handlers end with a slash:
"cert/": (*Handler).serveCert,
"profiles/": (*Handler).serveProfiles,
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME