// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package cli import ( "bytes" "context" "crypto/tls" "flag" "fmt" "log" "net/http" "os" "runtime" "strings" "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/atomicfile" "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/version" ) var certCmd = &ffcli.Command{ Name: "cert", Exec: runCert, ShortHelp: "get TLS certs", ShortUsage: "cert [flags] ", FlagSet: (func() *flag.FlagSet { fs := newFlagSet("cert") fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset") fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset") fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk") return fs })(), } var certArgs struct { certFile string keyFile string serve bool } func runCert(ctx context.Context, args []string) error { if certArgs.serve { s := &http.Server{ TLSConfig: &tls.Config{ GetCertificate: tailscale.GetCertificate, }, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" { if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok { http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect) return } } fmt.Fprintf(w, "

Hello from Tailscale

It works.") }), } log.Printf("running TLS server on :443 ...") return s.ListenAndServeTLS("", "") } if len(args) != 1 { var hint bytes.Buffer if st, err := tailscale.Status(ctx); err == nil { if st.BackendState != ipn.Running.String() { fmt.Fprintf(&hint, "\nTailscale is not running.\n") } else if len(st.CertDomains) == 0 { fmt.Fprintf(&hint, "\nHTTPS cert support is not enabled/configured for your tailnet.\n") } else if len(st.CertDomains) == 1 { fmt.Fprintf(&hint, "\nFor domain, use %q.\n", st.CertDomains[0]) } else { fmt.Fprintf(&hint, "\nValid domain options: %q.\n", st.CertDomains) } } return fmt.Errorf("Usage: tailscale cert [flags] %s", hint.Bytes()) } domain := args[0] printf := func(format string, a ...interface{}) { fmt.Printf(format, a...) } if certArgs.certFile == "-" || certArgs.keyFile == "-" { printf = log.Printf log.SetFlags(0) } if certArgs.certFile == "" && certArgs.keyFile == "" { certArgs.certFile = domain + ".crt" certArgs.keyFile = domain + ".key" } certPEM, keyPEM, err := tailscale.CertPair(ctx, domain) if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" { return fmt.Errorf("%v\n\nUse 'sudo tailscale cert' or 'tailscale up --operator=$USER' to not require root.", err) } if err != nil { return err } needMacWarning := version.IsSandboxedMacOS() macWarn := func() { if !needMacWarning { return } needMacWarning = false dir := "io.tailscale.ipn.macos" if version.IsMacSysExt() { dir = "io.tailscale.ipn.macsys" } printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s\n", dir) } if certArgs.certFile != "" { certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644) if err != nil { return err } if certArgs.certFile != "-" { macWarn() if certChanged { printf("Wrote public cert to %v\n", certArgs.certFile) } else { printf("Public cert unchanged at %v\n", certArgs.certFile) } } } if certArgs.keyFile != "" { keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600) if err != nil { return err } if certArgs.keyFile != "-" { macWarn() if keyChanged { printf("Wrote private key to %v\n", certArgs.keyFile) } else { printf("Private key unchanged at %v\n", certArgs.keyFile) } } } return nil } func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) { if filename == "-" { os.Stdout.Write(contents) return false, nil } if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) { return false, nil } if err := atomicfile.WriteFile(filename, contents, mode); err != nil { return false, err } return true, nil }