~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
195 lines · 6.2 KB Raw History
package tenantcredentialmain

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"k8s.io/client-go/rest"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/clusterkcp"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/foldmain"
)

const caCertName = "ca.crt"

const clientCertName = "tls.crt"

const clientKeyName = "tls.key"

func Main(ctx context.Context, inv foldmain.Invocation) int {
	if err := run(ctx, inv); err != nil {
		fmt.Fprintln(inv.Stderr, "socialweb-tenantcredential:", err)
		return 1
	}
	return 0
}

func run(ctx context.Context, inv foldmain.Invocation) error {
	cfg, err := load(inv.Env)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(cfg.OutDir, 0o700); err != nil {
		return fmt.Errorf("creating the out dir %s: %w", cfg.OutDir, err)
	}
	if err := refuseExisting(cfg.OutDir); err != nil {
		return err
	}
	restCfg, err := clusterkcp.MintTenantCredentialForClusterAs(
		ctx, cfg.StateDir, cfg.Cluster, cfg.Server, cfg.CommonName, cfg.Organizations, cfg.ValidFor)
	if err != nil {
		return err
	}
	leaf, err := identityOf(restCfg)
	if err != nil {
		return err
	}
	if err := checkIdentity(leaf, cfg.CommonName, cfg.Organizations); err != nil {
		return err
	}
	paths, err := writeCredential(cfg.OutDir, restCfg)
	if err != nil {
		return err
	}
	fmt.Fprintf(inv.Stdout, "socialweb-tenantcredential: cluster=%s commonName=%s organizations=%s validUntil=%s "+
		"ca=%s cert=%s key=%s\n", cfg.Cluster, cfg.CommonName, strings.Join(cfg.Organizations, ","),
		leaf.NotAfter.UTC().Format(time.RFC3339), paths[0], paths[1], paths[2])
	return nil
}

func refuseExisting(dir string) error {
	var found []string
	for _, name := range []string{caCertName, clientCertName, clientKeyName} {
		path := filepath.Join(dir, name)
		if _, err := os.Stat(path); err == nil {
			found = append(found, path)
		}
	}
	if len(found) > 0 {
		return fmt.Errorf(
			"these files already exist and this tool will not overwrite a credential: %s. "+
				"Overwriting would replace the credential some runner or controller is already "+
				"authenticating with, and that silent replacement is the failure invariant 20 "+
				"exists to prevent. Move the existing files aside, or name a different out dir",
			strings.Join(found, ", "))
	}
	return nil
}

func identityOf(cfg *rest.Config) (*x509.Certificate, error) {
	if len(cfg.TLSClientConfig.CertData) == 0 {
		return nil, errors.New("the minting path returned a credential with no client " +
			"certificate, so there is no identity to compare against what was asked for and " +
			"nothing to write to tls.crt")
	}
	for block, rest := pem.Decode(cfg.TLSClientConfig.CertData); block != nil; block, rest = pem.Decode(rest) {
		if block.Type != "CERTIFICATE" {
			continue
		}
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, fmt.Errorf("the minted client certificate does not parse: %w", err)
		}
		return cert, nil
	}
	return nil, errors.New("the minted client certificate holds no CERTIFICATE PEM block")
}

func checkIdentity(leaf *x509.Certificate, commonName string, organizations []string) error {
	if leaf.Subject.CommonName != commonName {
		return fmt.Errorf(
			"the minted credential authenticates as %q and %s asked for %q. The certificate is the "+
				"artifact: an operator whose RBAC binds %q would be handed a credential for %q, and "+
				"every rule keyed on it would miss. This tool refuses rather than write a credential "+
				"whose identity is not the one that was asked for",
			leaf.Subject.CommonName, envCommonName, commonName, commonName, leaf.Subject.CommonName)
	}
	if !slices.Equal(leaf.Subject.Organization, organizations) {
		return fmt.Errorf(
			"the minted credential carries the organizations %v and %s asked for %v. The apiserver "+
				"reads the groups off the Organizations, so a mismatch is a credential whose groups "+
				"the operator's RBAC does not bind: this tool refuses rather than write it",
			leaf.Subject.Organization, envOrganizations, organizations)
	}
	return nil
}

func writeCredential(dir string, cfg *rest.Config) ([3]string, error) {
	files := [3]struct {
		name string

		data []byte

		why string
	}{
		{caCertName, cfg.CAData,
			"the cluster's server CA, which is what verifies the apiserver's own certificate"},
		{clientCertName, cfg.TLSClientConfig.CertData, "the minted client certificate"},
		{clientKeyName, cfg.TLSClientConfig.KeyData, "the minted client key"},
	}
	var missing []string
	for _, f := range files {
		if len(f.data) == 0 {
			missing = append(missing, fmt.Sprintf("%s (%s)", f.name, f.why))
		}
	}
	if len(missing) > 0 {
		return [3]string{}, fmt.Errorf(
			"the minted credential is missing %s. The three files are a set and a partial one is "+
				"refused rather than written: ca.crt without tls.crt has nothing to present, and "+
				"tls.key without tls.crt is a private key that authenticates nothing, so a reader "+
				"of the directory would find a credential that looks complete and is not",
			strings.Join(missing, " and "))
	}

	var paths [3]string
	written := make([]string, 0, len(files))
	for i, f := range files {
		path, err := writeOne(dir, f.name, f.data)
		if err != nil {
			for _, done := range written {
				_ = os.Remove(done)
			}
			return [3]string{}, err
		}
		paths[i] = path
		written = append(written, path)
	}
	return paths, nil
}

func writeOne(dir, name string, data []byte) (string, error) {
	tmp, err := os.CreateTemp(dir, name+".tmp-*")
	if err != nil {
		return "", fmt.Errorf("creating a temporary file for %s in %s: %w", name, dir, err)
	}
	tmpName := tmp.Name()
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		return "", fmt.Errorf("writing %s: %w", tmpName, err)
	}
	if err := tmp.Chmod(0o600); err != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		return "", fmt.Errorf("setting %s to mode 0600: %w", tmpName, err)
	}
	if err := tmp.Close(); err != nil {
		_ = os.Remove(tmpName)
		return "", fmt.Errorf("closing %s: %w", tmpName, err)
	}
	final := filepath.Join(dir, name)
	if err := os.Rename(tmpName, final); err != nil {
		_ = os.Remove(tmpName)
		return "", fmt.Errorf("renaming %s into place as %s: %w", tmpName, final, err)
	}
	return final, nil
}