mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-08 09:07:44 +00:00
76904b82e7
This implements the same functionality as the former run.sh, but in Go and with a little better awareness of tailscaled's lifecycle. Also adds TS_AUTH_ONCE, which fixes the unfortunate behavior run.sh had where it would unconditionally try to reauth every time if you gave it an authkey, rather than try to use it only if auth is actually needed. This makes it a bit nicer to deploy these containers in automation, since you don't have to run the container once, then go and edit its definition to remove authkeys. Signed-off-by: David Anderson <danderson@tailscale.com>
191 lines
5.0 KiB
Go
191 lines
5.0 KiB
Go
// Copyright (c) 2022 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.
|
|
|
|
//go:build linux
|
|
// +build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// findKeyInKubeSecret inspects the kube secret secretName for a data
|
|
// field called "authkey", and returns its value if present.
|
|
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
|
|
kubeOnce.Do(initKube)
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resp, err := doKubeRequest(ctx, req)
|
|
if err != nil {
|
|
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
|
// Kube secret doesn't exist yet, can't have an authkey.
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
bs, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// We use a map[string]any here rather than import corev1.Secret,
|
|
// because we only do very limited things to the secret, and
|
|
// importing corev1 adds 12MiB to the compiled binary.
|
|
var s map[string]any
|
|
if err := json.Unmarshal(bs, &s); err != nil {
|
|
return "", err
|
|
}
|
|
if d, ok := s["data"].(map[string]any); ok {
|
|
if v, ok := d["authkey"].(string); ok {
|
|
bs, err := base64.StdEncoding.DecodeString(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(bs), nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// storeDeviceID writes deviceID into the "device_id" data field of
|
|
// the kube secret secretName.
|
|
func storeDeviceID(ctx context.Context, secretName, deviceID string) error {
|
|
kubeOnce.Do(initKube)
|
|
m := map[string]map[string]string{
|
|
"stringData": map[string]string{
|
|
"device_id": deviceID,
|
|
},
|
|
}
|
|
var b bytes.Buffer
|
|
if err := json.NewEncoder(&b).Encode(m); err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
|
|
if _, err := doKubeRequest(ctx, req); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// deleteAuthKey deletes the 'authkey' field of the given kube
|
|
// secret. No-op if there is no authkey in the secret.
|
|
func deleteAuthKey(ctx context.Context, secretName string) error {
|
|
kubeOnce.Do(initKube)
|
|
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
|
|
m := []struct {
|
|
Op string `json:"op"`
|
|
Path string `json:"path"`
|
|
}{
|
|
{
|
|
Op: "remove",
|
|
Path: "/data/authkey",
|
|
},
|
|
}
|
|
var b bytes.Buffer
|
|
if err := json.NewEncoder(&b).Encode(m); err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json-patch+json")
|
|
if resp, err := doKubeRequest(ctx, req); err != nil {
|
|
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
|
|
// This is kubernetes-ese for "the field you asked to
|
|
// delete already doesn't exist", aka no-op.
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
kubeOnce sync.Once
|
|
kubeHost string
|
|
kubeNamespace string
|
|
kubeToken string
|
|
kubeHTTP *http.Transport
|
|
)
|
|
|
|
func initKube() {
|
|
// If running in Kubernetes, set things up so that doKubeRequest
|
|
// can talk successfully to the kube apiserver.
|
|
if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
|
|
return
|
|
}
|
|
|
|
kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
|
|
|
|
bs, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
|
if err != nil {
|
|
log.Fatalf("Error reading kube namespace: %v", err)
|
|
}
|
|
kubeNamespace = strings.TrimSpace(string(bs))
|
|
|
|
bs, err = os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
|
if err != nil {
|
|
log.Fatalf("Error reading kube token: %v", err)
|
|
}
|
|
kubeToken = strings.TrimSpace(string(bs))
|
|
|
|
bs, err = os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
|
|
if err != nil {
|
|
log.Fatalf("Error reading kube CA cert: %v", err)
|
|
}
|
|
cp := x509.NewCertPool()
|
|
cp.AppendCertsFromPEM(bs)
|
|
kubeHTTP = &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: cp,
|
|
},
|
|
IdleConnTimeout: time.Second,
|
|
}
|
|
}
|
|
|
|
// doKubeRequest sends r to the kube apiserver.
|
|
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
|
|
kubeOnce.Do(initKube)
|
|
if kubeHTTP == nil {
|
|
panic("not in kubernetes")
|
|
}
|
|
|
|
r.URL.Scheme = "https"
|
|
r.URL.Host = kubeHost
|
|
r.Header.Set("Authorization", "Bearer "+kubeToken)
|
|
r.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := kubeHTTP.RoundTrip(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return resp, fmt.Errorf("got non-200 status code %d", resp.StatusCode)
|
|
}
|
|
return resp, nil
|
|
}
|