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:
committed by
Brad Fitzpatrick
parent
99b3f69126
commit
e180fc267b
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user