mirror of
https://github.com/tailscale/tailscale.git
synced 2025-09-21 05:31:36 +02:00
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:
parent
99b3f69126
commit
e180fc267b
@ -41,7 +41,7 @@ while [ "$#" -gt 1 ]; do
|
|||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
ldflags="$ldflags -w -s"
|
ldflags="$ldflags -w -s"
|
||||||
tags="${tags:+$tags,},$($go run ./cmd/featuretags --min)"
|
tags="${tags:+$tags,},$(GOOS= GOARCH= $go run ./cmd/featuretags --min)"
|
||||||
;;
|
;;
|
||||||
--box)
|
--box)
|
||||||
if [ ! -z "${TAGS:-}" ]; then
|
if [ ! -z "${TAGS:-}" ]; then
|
||||||
|
151
client/local/cert.go
Normal file
151
client/local/cert.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !js && !ts_omit_acme
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go4.org/mem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetDNS adds a DNS TXT record for the given domain name, containing
|
||||||
|
// the provided TXT value. The intended use case is answering
|
||||||
|
// LetsEncrypt/ACME dns-01 challenges.
|
||||||
|
//
|
||||||
|
// The control plane will only permit SetDNS requests with very
|
||||||
|
// specific names and values. The name should be
|
||||||
|
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
||||||
|
// clients cache the certs from LetsEncrypt (or whichever CA is
|
||||||
|
// providing them) and only request new ones as needed; the control plane
|
||||||
|
// rate limits SetDNS requests.
|
||||||
|
//
|
||||||
|
// This is a low-level interface; it's expected that most Tailscale
|
||||||
|
// users use a higher level interface to getting/using TLS
|
||||||
|
// certificates.
|
||||||
|
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Set("name", name)
|
||||||
|
v.Set("value", value)
|
||||||
|
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertPair returns a cert and private key for the provided DNS domain.
|
||||||
|
//
|
||||||
|
// It returns a cached certificate from disk if it's still valid.
|
||||||
|
//
|
||||||
|
// Deprecated: use [Client.CertPair].
|
||||||
|
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||||
|
return defaultClient.CertPair(ctx, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertPair returns a cert and private key for the provided DNS domain.
|
||||||
|
//
|
||||||
|
// It returns a cached certificate from disk if it's still valid.
|
||||||
|
//
|
||||||
|
// API maturity: this is considered a stable API.
|
||||||
|
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||||
|
return lc.CertPairWithValidity(ctx, domain, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertPairWithValidity returns a cert and private key for the provided DNS
|
||||||
|
// domain.
|
||||||
|
//
|
||||||
|
// It returns a cached certificate from disk if it's still valid.
|
||||||
|
// When minValidity is non-zero, the returned certificate will be valid for at
|
||||||
|
// least the given duration, if permitted by the CA. If the certificate is
|
||||||
|
// valid, but for less than minValidity, it will be synchronously renewed.
|
||||||
|
//
|
||||||
|
// API maturity: this is considered a stable API.
|
||||||
|
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
|
||||||
|
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
// with ?type=pair, the response PEM is first the one private
|
||||||
|
// key PEM block, then the cert PEM blocks.
|
||||||
|
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
||||||
|
if i == -1 {
|
||||||
|
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
||||||
|
}
|
||||||
|
i += len("--\n")
|
||||||
|
keyPEM, certPEM = res[:i], res[i:]
|
||||||
|
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
||||||
|
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
||||||
|
}
|
||||||
|
return certPEM, keyPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||||
|
//
|
||||||
|
// It returns a cached certificate from disk if it's still valid.
|
||||||
|
//
|
||||||
|
// It's the right signature to use as the value of
|
||||||
|
// [tls.Config.GetCertificate].
|
||||||
|
//
|
||||||
|
// Deprecated: use [Client.GetCertificate].
|
||||||
|
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
return defaultClient.GetCertificate(hi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||||
|
//
|
||||||
|
// It returns a cached certificate from disk if it's still valid.
|
||||||
|
//
|
||||||
|
// It's the right signature to use as the value of
|
||||||
|
// [tls.Config.GetCertificate].
|
||||||
|
//
|
||||||
|
// API maturity: this is considered a stable API.
|
||||||
|
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if hi == nil || hi.ServerName == "" {
|
||||||
|
return nil, errors.New("no SNI ServerName")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
name := hi.ServerName
|
||||||
|
if !strings.Contains(name, ".") {
|
||||||
|
if v, ok := lc.ExpandSNIName(ctx, name); ok {
|
||||||
|
name = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
certPEM, keyPEM, err := lc.CertPair(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||||
|
//
|
||||||
|
// Deprecated: use [Client.ExpandSNIName].
|
||||||
|
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||||
|
return defaultClient.ExpandSNIName(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||||
|
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||||
|
st, err := lc.StatusWithoutPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for _, d := range st.CertDomains {
|
||||||
|
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
||||||
|
return d, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
@ -9,7 +9,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@ -28,7 +27,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go4.org/mem"
|
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
@ -907,28 +905,6 @@ func (lc *Client) Logout(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDNS adds a DNS TXT record for the given domain name, containing
|
|
||||||
// the provided TXT value. The intended use case is answering
|
|
||||||
// LetsEncrypt/ACME dns-01 challenges.
|
|
||||||
//
|
|
||||||
// The control plane will only permit SetDNS requests with very
|
|
||||||
// specific names and values. The name should be
|
|
||||||
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
|
||||||
// clients cache the certs from LetsEncrypt (or whichever CA is
|
|
||||||
// providing them) and only request new ones as needed; the control plane
|
|
||||||
// rate limits SetDNS requests.
|
|
||||||
//
|
|
||||||
// This is a low-level interface; it's expected that most Tailscale
|
|
||||||
// users use a higher level interface to getting/using TLS
|
|
||||||
// certificates.
|
|
||||||
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("name", name)
|
|
||||||
v.Set("value", value)
|
|
||||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialTCP connects to the host's port via Tailscale.
|
// DialTCP connects to the host's port via Tailscale.
|
||||||
//
|
//
|
||||||
// The host may be a base DNS name (resolved from the netmap inside
|
// The host may be a base DNS name (resolved from the netmap inside
|
||||||
@ -1009,117 +985,6 @@ func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
|
|||||||
return &derpMap, nil
|
return &derpMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertPair returns a cert and private key for the provided DNS domain.
|
|
||||||
//
|
|
||||||
// It returns a cached certificate from disk if it's still valid.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Client.CertPair].
|
|
||||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
|
||||||
return defaultClient.CertPair(ctx, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertPair returns a cert and private key for the provided DNS domain.
|
|
||||||
//
|
|
||||||
// It returns a cached certificate from disk if it's still valid.
|
|
||||||
//
|
|
||||||
// API maturity: this is considered a stable API.
|
|
||||||
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
|
||||||
return lc.CertPairWithValidity(ctx, domain, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertPairWithValidity returns a cert and private key for the provided DNS
|
|
||||||
// domain.
|
|
||||||
//
|
|
||||||
// It returns a cached certificate from disk if it's still valid.
|
|
||||||
// When minValidity is non-zero, the returned certificate will be valid for at
|
|
||||||
// least the given duration, if permitted by the CA. If the certificate is
|
|
||||||
// valid, but for less than minValidity, it will be synchronously renewed.
|
|
||||||
//
|
|
||||||
// API maturity: this is considered a stable API.
|
|
||||||
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
|
|
||||||
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
// with ?type=pair, the response PEM is first the one private
|
|
||||||
// key PEM block, then the cert PEM blocks.
|
|
||||||
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
|
||||||
if i == -1 {
|
|
||||||
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
|
||||||
}
|
|
||||||
i += len("--\n")
|
|
||||||
keyPEM, certPEM = res[:i], res[i:]
|
|
||||||
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
|
||||||
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
|
||||||
}
|
|
||||||
return certPEM, keyPEM, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
|
||||||
//
|
|
||||||
// It returns a cached certificate from disk if it's still valid.
|
|
||||||
//
|
|
||||||
// It's the right signature to use as the value of
|
|
||||||
// [tls.Config.GetCertificate].
|
|
||||||
//
|
|
||||||
// Deprecated: use [Client.GetCertificate].
|
|
||||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
return defaultClient.GetCertificate(hi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
|
||||||
//
|
|
||||||
// It returns a cached certificate from disk if it's still valid.
|
|
||||||
//
|
|
||||||
// It's the right signature to use as the value of
|
|
||||||
// [tls.Config.GetCertificate].
|
|
||||||
//
|
|
||||||
// API maturity: this is considered a stable API.
|
|
||||||
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
if hi == nil || hi.ServerName == "" {
|
|
||||||
return nil, errors.New("no SNI ServerName")
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
name := hi.ServerName
|
|
||||||
if !strings.Contains(name, ".") {
|
|
||||||
if v, ok := lc.ExpandSNIName(ctx, name); ok {
|
|
||||||
name = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
certPEM, keyPEM, err := lc.CertPair(ctx, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Client.ExpandSNIName].
|
|
||||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
|
||||||
return defaultClient.ExpandSNIName(ctx, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
|
||||||
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
|
||||||
st, err := lc.StatusWithoutPeers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
for _, d := range st.CertDomains {
|
|
||||||
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
|
||||||
return d, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingOpts contains options for the ping request.
|
// PingOpts contains options for the ping request.
|
||||||
//
|
//
|
||||||
// The zero value is valid, which means to use defaults.
|
// The zero value is valid, which means to use defaults.
|
||||||
|
34
client/tailscale/cert.go
Normal file
34
client/tailscale/cert.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !js && !ts_omit_acme
|
||||||
|
|
||||||
|
package tailscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"tailscale.com/client/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCertificate is an alias for [tailscale.com/client/local.GetCertificate].
|
||||||
|
//
|
||||||
|
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.GetCertificate].
|
||||||
|
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
return local.GetCertificate(hi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertPair is an alias for [tailscale.com/client/local.CertPair].
|
||||||
|
//
|
||||||
|
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.CertPair].
|
||||||
|
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||||
|
return local.CertPair(ctx, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandSNIName is an alias for [tailscale.com/client/local.ExpandSNIName].
|
||||||
|
//
|
||||||
|
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.ExpandSNIName].
|
||||||
|
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||||
|
return local.ExpandSNIName(ctx, name)
|
||||||
|
}
|
@ -5,7 +5,6 @@ package tailscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
@ -37,13 +36,6 @@ type BugReportOpts = local.BugReportOpts
|
|||||||
// Deprecated: import [tailscale.com/client/local] instead.
|
// Deprecated: import [tailscale.com/client/local] instead.
|
||||||
type PingOpts = local.PingOpts
|
type PingOpts = local.PingOpts
|
||||||
|
|
||||||
// GetCertificate is an alias for [tailscale.com/client/local.GetCertificate].
|
|
||||||
//
|
|
||||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.GetCertificate].
|
|
||||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
return local.GetCertificate(hi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetVersionMismatchHandler is an alias for [tailscale.com/client/local.SetVersionMismatchHandler].
|
// SetVersionMismatchHandler is an alias for [tailscale.com/client/local.SetVersionMismatchHandler].
|
||||||
//
|
//
|
||||||
// Deprecated: import [tailscale.com/client/local] instead.
|
// Deprecated: import [tailscale.com/client/local] instead.
|
||||||
@ -85,17 +77,3 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
|
|||||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||||
return local.StatusWithoutPeers(ctx)
|
return local.StatusWithoutPeers(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertPair is an alias for [tailscale.com/client/local.CertPair].
|
|
||||||
//
|
|
||||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.CertPair].
|
|
||||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
|
||||||
return local.CertPair(ctx, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandSNIName is an alias for [tailscale.com/client/local.ExpandSNIName].
|
|
||||||
//
|
|
||||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.ExpandSNIName].
|
|
||||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
|
||||||
return local.ExpandSNIName(ctx, name)
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !js && !ts_omit_acme
|
||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -25,7 +27,9 @@ import (
|
|||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var certCmd = &ffcli.Command{
|
func init() {
|
||||||
|
maybeCertCmd = func() *ffcli.Command {
|
||||||
|
return &ffcli.Command{
|
||||||
Name: "cert",
|
Name: "cert",
|
||||||
Exec: runCert,
|
Exec: runCert,
|
||||||
ShortHelp: "Get TLS certs",
|
ShortHelp: "Get TLS certs",
|
||||||
@ -38,6 +42,8 @@ var certCmd = &ffcli.Command{
|
|||||||
fs.DurationVar(&certArgs.minValidity, "min-validity", 0, "ensure the certificate is valid for at least this duration; the output certificate is never expired if this flag is unset or 0, but the lifetime may vary; the maximum allowed min-validity depends on the CA")
|
fs.DurationVar(&certArgs.minValidity, "min-validity", 0, "ensure the certificate is valid for at least this duration; the output certificate is never expired if this flag is unset or 0, but the lifetime may vary; the maximum allowed min-validity depends on the CA")
|
||||||
return fs
|
return fs
|
||||||
})(),
|
})(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var certArgs struct {
|
var certArgs struct {
|
||||||
|
@ -215,6 +215,7 @@ var (
|
|||||||
maybeNetlockCmd,
|
maybeNetlockCmd,
|
||||||
maybeFunnelCmd,
|
maybeFunnelCmd,
|
||||||
maybeServeCmd,
|
maybeServeCmd,
|
||||||
|
maybeCertCmd,
|
||||||
_ func() *ffcli.Command
|
_ func() *ffcli.Command
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -262,7 +263,7 @@ change in the future.
|
|||||||
nilOrCall(maybeWebCmd),
|
nilOrCall(maybeWebCmd),
|
||||||
nilOrCall(fileCmd),
|
nilOrCall(fileCmd),
|
||||||
bugReportCmd,
|
bugReportCmd,
|
||||||
certCmd,
|
nilOrCall(maybeCertCmd),
|
||||||
nilOrCall(maybeNetlockCmd),
|
nilOrCall(maybeNetlockCmd),
|
||||||
licensesCmd,
|
licensesCmd,
|
||||||
exitNodeCmd(),
|
exitNodeCmd(),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build linux && !ts_omit_acme
|
||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -22,6 +24,10 @@ import (
|
|||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
maybeConfigSynologyCertCmd = synologyConfigureCertCmd
|
||||||
|
}
|
||||||
|
|
||||||
func synologyConfigureCertCmd() *ffcli.Command {
|
func synologyConfigureCertCmd() *ffcli.Command {
|
||||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build linux && !ts_omit_acme
|
||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,7 +10,11 @@ import (
|
|||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var maybeJetKVMConfigureCmd func() *ffcli.Command // non-nil only on Linux/arm for JetKVM
|
var (
|
||||||
|
maybeJetKVMConfigureCmd,
|
||||||
|
maybeConfigSynologyCertCmd,
|
||||||
|
_ func() *ffcli.Command // non-nil only on Linux/arm for JetKVM
|
||||||
|
)
|
||||||
|
|
||||||
func configureCmd() *ffcli.Command {
|
func configureCmd() *ffcli.Command {
|
||||||
return &ffcli.Command{
|
return &ffcli.Command{
|
||||||
@ -28,7 +32,7 @@ services on the host to use Tailscale in more ways.
|
|||||||
Subcommands: nonNilCmds(
|
Subcommands: nonNilCmds(
|
||||||
configureKubeconfigCmd(),
|
configureKubeconfigCmd(),
|
||||||
synologyConfigureCmd(),
|
synologyConfigureCmd(),
|
||||||
synologyConfigureCertCmd(),
|
ccall(maybeConfigSynologyCertCmd),
|
||||||
ccall(maybeSysExtCmd),
|
ccall(maybeSysExtCmd),
|
||||||
ccall(maybeVPNConfigCmd),
|
ccall(maybeVPNConfigCmd),
|
||||||
ccall(maybeJetKVMConfigureCmd),
|
ccall(maybeJetKVMConfigureCmd),
|
||||||
|
@ -108,3 +108,16 @@ func TestOmitPortmapper(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}.Check(t)
|
}.Check(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOmitACME(t *testing.T) {
|
||||||
|
deptest.DepChecker{
|
||||||
|
GOOS: "linux",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Tags: "ts_omit_acme,ts_include_cli",
|
||||||
|
OnDep: func(dep string) {
|
||||||
|
if strings.Contains(dep, "/acme") {
|
||||||
|
t.Errorf("unexpected dep with ts_omit_acme: %q", dep)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}.Check(t)
|
||||||
|
}
|
||||||
|
13
feature/buildfeatures/feature_acme_disabled.go
Normal file
13
feature/buildfeatures/feature_acme_disabled.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Code generated by gen.go; DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build ts_omit_acme
|
||||||
|
|
||||||
|
package buildfeatures
|
||||||
|
|
||||||
|
// HasACME is whether the binary was built with support for modular feature "ACME TLS certificate management".
|
||||||
|
// Specifically, it's whether the binary was NOT built with the "ts_omit_acme" build tag.
|
||||||
|
// It's a const so it can be used for dead code elimination.
|
||||||
|
const HasACME = false
|
13
feature/buildfeatures/feature_acme_enabled.go
Normal file
13
feature/buildfeatures/feature_acme_enabled.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Code generated by gen.go; DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build !ts_omit_acme
|
||||||
|
|
||||||
|
package buildfeatures
|
||||||
|
|
||||||
|
// HasACME is whether the binary was built with support for modular feature "ACME TLS certificate management".
|
||||||
|
// Specifically, it's whether the binary was NOT built with the "ts_omit_acme" build tag.
|
||||||
|
// It's a const so it can be used for dead code elimination.
|
||||||
|
const HasACME = true
|
@ -42,6 +42,7 @@ type FeatureMeta struct {
|
|||||||
// Features are the known Tailscale features that can be selectively included or
|
// Features are the known Tailscale features that can be selectively included or
|
||||||
// excluded via build tags, and a description of each.
|
// excluded via build tags, and a description of each.
|
||||||
var Features = map[FeatureTag]FeatureMeta{
|
var Features = map[FeatureTag]FeatureMeta{
|
||||||
|
"acme": {"ACME", "ACME TLS certificate management"},
|
||||||
"aws": {"AWS", "AWS integration"},
|
"aws": {"AWS", "AWS integration"},
|
||||||
"bird": {"Bird", "Bird BGP integration"},
|
"bird": {"Bird", "Bird BGP integration"},
|
||||||
"capture": {"Capture", "Packet capture"},
|
"capture": {"Capture", "Packet capture"},
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -54,9 +52,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
|
|||||||
req("POST /logtail/flush"): handleC2NLogtailFlush,
|
req("POST /logtail/flush"): handleC2NLogtailFlush,
|
||||||
req("POST /sockstats"): handleC2NSockStats,
|
req("POST /sockstats"): handleC2NSockStats,
|
||||||
|
|
||||||
// Check TLS certificate status.
|
|
||||||
req("GET /tls-cert-status"): handleC2NTLSCertStatus,
|
|
||||||
|
|
||||||
// SSH
|
// SSH
|
||||||
req("/ssh/usernames"): handleC2NSSHUsernames,
|
req("/ssh/usernames"): handleC2NSSHUsernames,
|
||||||
|
|
||||||
@ -497,54 +492,3 @@ func regularFileExists(path string) bool {
|
|||||||
fi, err := os.Stat(path)
|
fi, err := os.Stat(path)
|
||||||
return err == nil && fi.Mode().IsRegular()
|
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)
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build !js
|
//go:build !js && !ts_omit_acme
|
||||||
|
|
||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
randv2 "math/rand/v2"
|
randv2 "math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -40,6 +41,7 @@ import (
|
|||||||
"tailscale.com/ipn/store"
|
"tailscale.com/ipn/store"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/net/bakedroots"
|
"tailscale.com/net/bakedroots"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tempfork/acme"
|
"tailscale.com/tempfork/acme"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/testenv"
|
"tailscale.com/util/testenv"
|
||||||
@ -47,6 +49,10 @@ import (
|
|||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterC2N("GET /tls-cert-status", handleC2NTLSCertStatus)
|
||||||
|
}
|
||||||
|
|
||||||
// Process-wide cache. (A new *Handler is created per connection,
|
// Process-wide cache. (A new *Handler is created per connection,
|
||||||
// effectively per request)
|
// effectively per request)
|
||||||
var (
|
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)
|
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
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build js || ts_omit_acme
|
||||||
|
|
||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterC2N("GET /tls-cert-status", handleC2NTLSCertStatusDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNoCerts = errors.New("cert support not compiled in this build")
|
||||||
|
|
||||||
type TLSCertKeyPair struct {
|
type TLSCertKeyPair struct {
|
||||||
CertPEM, KeyPEM []byte
|
CertPEM, KeyPEM []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
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")
|
var errCertExpired = errors.New("cert expired")
|
||||||
@ -22,9 +32,14 @@ var errCertExpired = errors.New("cert expired")
|
|||||||
type certStore interface{}
|
type certStore interface{}
|
||||||
|
|
||||||
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
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) {
|
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
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build !ios && !android && !js
|
//go:build !ios && !android && !js && !ts_omit_acme
|
||||||
|
|
||||||
package localapi
|
package localapi
|
||||||
|
|
||||||
@ -14,6 +14,10 @@ import (
|
|||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("cert/", (*Handler).serveCert)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.PermitWrite && !h.PermitCert {
|
if !h.PermitWrite && !h.PermitCert {
|
||||||
http.Error(w, "cert access denied", http.StatusForbidden)
|
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.
|
// then it's a prefix match.
|
||||||
var handler = map[string]LocalAPIHandler{
|
var handler = map[string]LocalAPIHandler{
|
||||||
// The prefix match handlers end with a slash:
|
// The prefix match handlers end with a slash:
|
||||||
"cert/": (*Handler).serveCert,
|
|
||||||
"profiles/": (*Handler).serveProfiles,
|
"profiles/": (*Handler).serveProfiles,
|
||||||
|
|
||||||
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
||||||
|
Loading…
x
Reference in New Issue
Block a user