mirror of
https://github.com/kairos-io/kcrypt-challenger.git
synced 2025-04-27 19:05:23 +00:00
- Flatten if/else logic by handling errors and returning early - Use different logger for server logs. Also handle skipped errors. - Remove unecessary for loop - --zap-log-level can already be used (and it works) - Remove non-existent enki flag - Run tests with KVM enabled on self-hosted runners and also don't add grub.cfg since it's already there in the base image - Remove non-used earthly target Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
396 lines
11 KiB
Go
396 lines
11 KiB
Go
package challenger
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
|
|
keyserverv1alpha1 "github.com/kairos-io/kairos-challenger/api/v1alpha1"
|
|
"github.com/kairos-io/kairos-challenger/pkg/constants"
|
|
"github.com/kairos-io/kairos-challenger/pkg/payload"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"github.com/kairos-io/kairos-challenger/controllers"
|
|
tpm "github.com/kairos-io/tpm-helpers"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// PassphraseRequestData is a struct that holds all the information needed in
|
|
// order to lookup a passphrase for a specific tpm hash.
|
|
type PassphraseRequestData struct {
|
|
TPMHash string
|
|
Label string
|
|
DeviceName string
|
|
UUID string
|
|
}
|
|
|
|
type SealedVolumeData struct {
|
|
Quarantined bool
|
|
SecretName string
|
|
SecretPath string
|
|
|
|
PartitionLabel string
|
|
VolumeName string
|
|
}
|
|
|
|
var upgrader = websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
}
|
|
|
|
func cleanKubeName(s string) (d string) {
|
|
d = strings.ReplaceAll(s, "_", "-")
|
|
d = strings.ToLower(d)
|
|
return
|
|
}
|
|
|
|
func (s SealedVolumeData) DefaultSecret() (string, string) {
|
|
secretName := fmt.Sprintf("%s-%s", s.VolumeName, s.PartitionLabel)
|
|
secretPath := "passphrase"
|
|
if s.SecretName != "" {
|
|
secretName = s.SecretName
|
|
}
|
|
if s.SecretPath != "" {
|
|
secretPath = s.SecretPath
|
|
}
|
|
return cleanKubeName(secretName), cleanKubeName(secretPath)
|
|
}
|
|
|
|
func writeRead(conn *websocket.Conn, input []byte) ([]byte, error) {
|
|
writer, err := conn.NextWriter(websocket.BinaryMessage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := writer.Write(input); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := writer.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, reader, err := conn.NextReader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ioutil.ReadAll(reader)
|
|
}
|
|
|
|
func getPubHash(token string) (string, error) {
|
|
ek, _, err := tpm.GetAttestationData(token)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return tpm.DecodePubHash(ek)
|
|
}
|
|
|
|
func Start(ctx context.Context, logger logr.Logger, kclient *kubernetes.Clientset, reconciler *controllers.SealedVolumeReconciler, namespace, address string) {
|
|
logger.Info("Challenger started", "address", address)
|
|
s := http.Server{
|
|
Addr: address,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
}
|
|
|
|
m := http.NewServeMux()
|
|
|
|
m.HandleFunc("/postPass", func(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
logger.Error(err, "upgrading connection")
|
|
return
|
|
}
|
|
defer func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
logger.Error(err, "closing the connection")
|
|
}
|
|
}()
|
|
|
|
logger.Info("Receiving passphrase")
|
|
if err := tpm.AuthRequest(r, conn); err != nil {
|
|
errorMessage(conn, logger, err, "auth request")
|
|
return
|
|
}
|
|
logger.Info("[Receiving passphrase] auth succeeded")
|
|
|
|
token := r.Header.Get("Authorization")
|
|
hashEncoded, err := getPubHash(token)
|
|
if err != nil {
|
|
errorMessage(conn, logger, err, "decoding pubhash")
|
|
return
|
|
}
|
|
logger.Info("[Receiving passphrase] pubhash", "encodedhash", hashEncoded)
|
|
|
|
label := r.Header.Get("label")
|
|
name := r.Header.Get("name")
|
|
uuid := r.Header.Get("uuid")
|
|
v := &payload.Data{}
|
|
logger.Info("Reading request data", "label", label, "name", name, "uuid", uuid)
|
|
|
|
volumeList := &keyserverv1alpha1.SealedVolumeList{}
|
|
for {
|
|
if err := reconciler.List(ctx, volumeList, &client.ListOptions{Namespace: namespace}); err != nil {
|
|
logger.Error(err, "listing volumes")
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
logger.Info("Looking up volume with request data")
|
|
sealedVolumeData := findVolumeFor(PassphraseRequestData{
|
|
TPMHash: hashEncoded,
|
|
Label: label,
|
|
DeviceName: name,
|
|
UUID: uuid,
|
|
}, volumeList)
|
|
|
|
if sealedVolumeData == nil {
|
|
errorMessage(conn, logger, fmt.Errorf("no TPM Hash found for %s", hashEncoded), "")
|
|
return
|
|
}
|
|
logger.Info("[Looking up volume with request data] succeeded")
|
|
|
|
if err := conn.ReadJSON(v); err != nil {
|
|
logger.Error(err, "reading json from connection")
|
|
return
|
|
}
|
|
|
|
if !v.HasPassphrase() {
|
|
errorMessage(conn, logger, fmt.Errorf("invalid answer from client: doesn't contain any passphrase"), "")
|
|
}
|
|
if v.HasError() {
|
|
errorMessage(conn, logger, fmt.Errorf("error: %s", v.Error), v.Error)
|
|
}
|
|
|
|
secretName, secretPath := sealedVolumeData.DefaultSecret()
|
|
logger.Info("Looking up secret in with name", "name", secretName, "namespace", namespace)
|
|
_, err = kclient.CoreV1().Secrets(namespace).Get(ctx, secretName, v1.GetOptions{})
|
|
if err == nil {
|
|
logger.Info("Posted for already existing secret - ignoring")
|
|
return
|
|
}
|
|
if !apierrors.IsNotFound(err) {
|
|
errorMessage(conn, logger, err, "failed getting secret")
|
|
return
|
|
}
|
|
|
|
logger.Info("secret not found, creating one")
|
|
secret := corev1.Secret{
|
|
TypeMeta: v1.TypeMeta{
|
|
Kind: "Secret",
|
|
APIVersion: "apps/v1",
|
|
},
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: secretName,
|
|
Namespace: namespace,
|
|
},
|
|
StringData: map[string]string{
|
|
secretPath: v.Passphrase,
|
|
constants.GeneratedByKey: v.GeneratedBy,
|
|
},
|
|
Type: "Opaque",
|
|
}
|
|
_, err = kclient.CoreV1().Secrets(namespace).Create(ctx, &secret, v1.CreateOptions{})
|
|
if err != nil {
|
|
errorMessage(conn, logger, err, "failed during secret creation")
|
|
}
|
|
logger.Info("created new secret")
|
|
})
|
|
|
|
m.HandleFunc("/getPass", func(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
logger.Error(err, "upgrading connection")
|
|
return
|
|
}
|
|
defer func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
logger.Error(err, "closing the connection")
|
|
}
|
|
}()
|
|
|
|
logger.Info("Received connection")
|
|
volumeList := &keyserverv1alpha1.SealedVolumeList{}
|
|
for {
|
|
if err := reconciler.List(ctx, volumeList, &client.ListOptions{Namespace: namespace}); err != nil {
|
|
logger.Error(err, "listing volumes")
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
logger.Info("reading data from request")
|
|
token := r.Header.Get("Authorization")
|
|
label := r.Header.Get("label")
|
|
name := r.Header.Get("name")
|
|
uuid := r.Header.Get("uuid")
|
|
|
|
tokenStr := "empty"
|
|
if token != "" {
|
|
tokenStr = "not empty"
|
|
}
|
|
logger.Info("request data", "token", tokenStr, "label", label, "name", name, "uuid", uuid)
|
|
|
|
if err := tpm.AuthRequest(r, conn); err != nil {
|
|
logger.Error(err, "error validating challenge")
|
|
return
|
|
}
|
|
|
|
hashEncoded, err := getPubHash(token)
|
|
if err != nil {
|
|
logger.Error(err, "error decoding pubhash")
|
|
return
|
|
}
|
|
|
|
logger.Info("Looking up volume with request data")
|
|
sealedVolumeData := findVolumeFor(PassphraseRequestData{
|
|
TPMHash: hashEncoded,
|
|
Label: label,
|
|
DeviceName: name,
|
|
UUID: uuid,
|
|
}, volumeList)
|
|
|
|
if sealedVolumeData == nil {
|
|
errorMessage(conn, logger, fmt.Errorf("no volume found with data from request and hash: %s", hashEncoded), "")
|
|
return
|
|
}
|
|
logger.Info("[Looking up volume with request data] succeeded")
|
|
|
|
if sealedVolumeData.Quarantined {
|
|
errorMessage(conn, logger, fmt.Errorf("quarantined: %s", sealedVolumeData.PartitionLabel), "")
|
|
return
|
|
}
|
|
|
|
secretName, secretPath := sealedVolumeData.DefaultSecret()
|
|
|
|
// 1. The admin sets a specific cleartext password from Kube manager
|
|
// SealedVolume -> with a secret .
|
|
// 2. The admin just adds a SealedVolume associated with a TPM Hash ( you don't provide any passphrase )
|
|
// 3. There is no challenger server at all (offline mode)
|
|
//
|
|
logger.Info(fmt.Sprintf("looking up secret %s in namespace %s", secretName, namespace))
|
|
secret, err := kclient.CoreV1().Secrets(namespace).Get(ctx, secretName, v1.GetOptions{})
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
errorMessage(conn, logger, fmt.Errorf("No secret found for %s and %s", hashEncoded, sealedVolumeData.PartitionLabel), "")
|
|
} else {
|
|
errorMessage(conn, logger, err, "getting the secret from Kubernetes")
|
|
}
|
|
|
|
return
|
|
}
|
|
logger.Info(fmt.Sprintf("secret %s found in namespace %s", secretName, namespace))
|
|
|
|
passphrase := secret.Data[secretPath]
|
|
generatedBy := secret.Data[constants.GeneratedByKey]
|
|
|
|
writer, err := conn.NextWriter(websocket.BinaryMessage)
|
|
if err != nil {
|
|
logger.Error(err, "getting a writer from the connection")
|
|
}
|
|
p := payload.Data{Passphrase: string(passphrase), GeneratedBy: string(generatedBy)}
|
|
err = json.NewEncoder(writer).Encode(p)
|
|
if err != nil {
|
|
logger.Error(err, "writing passphrase to the websocket channel")
|
|
}
|
|
if err = writer.Close(); err != nil {
|
|
logger.Error(err, "closing the writer")
|
|
return
|
|
}
|
|
})
|
|
|
|
s.Handler = logRequestHandler(logger, m)
|
|
|
|
go func() {
|
|
err := s.ListenAndServe()
|
|
if err != nil && err != http.ErrServerClosed {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
s.Shutdown(ctx)
|
|
}()
|
|
}
|
|
|
|
func findVolumeFor(requestData PassphraseRequestData, volumeList *keyserverv1alpha1.SealedVolumeList) *SealedVolumeData {
|
|
for _, v := range volumeList.Items {
|
|
if requestData.TPMHash == v.Spec.TPMHash {
|
|
for _, p := range v.Spec.Partitions {
|
|
deviceNameMatches := requestData.DeviceName != "" && p.DeviceName == requestData.DeviceName
|
|
uuidMatches := requestData.UUID != "" && p.UUID == requestData.UUID
|
|
labelMatches := requestData.Label != "" && p.Label == requestData.Label
|
|
secretName := ""
|
|
if p.Secret != nil && p.Secret.Name != "" {
|
|
secretName = p.Secret.Name
|
|
}
|
|
secretPath := ""
|
|
if p.Secret != nil && p.Secret.Path != "" {
|
|
secretPath = p.Secret.Path
|
|
}
|
|
if labelMatches || uuidMatches || deviceNameMatches {
|
|
return &SealedVolumeData{
|
|
Quarantined: v.Spec.Quarantined,
|
|
SecretName: secretName,
|
|
SecretPath: secretPath,
|
|
VolumeName: v.Name,
|
|
PartitionLabel: p.Label,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// errorMessage should be used when an error should be both, printed to the stdout
|
|
// and sent over the wire to the websocket client.
|
|
func errorMessage(conn *websocket.Conn, logger logr.Logger, theErr error, description string) {
|
|
if theErr == nil {
|
|
return
|
|
}
|
|
logger.Error(theErr, description)
|
|
|
|
writer, err := conn.NextWriter(websocket.BinaryMessage)
|
|
if err != nil {
|
|
logger.Error(err, "getting a writer from the connection")
|
|
}
|
|
|
|
errMsg := theErr.Error()
|
|
err = json.NewEncoder(writer).Encode(payload.Data{Error: errMsg})
|
|
if err != nil {
|
|
logger.Error(err, "error encoding the response to json")
|
|
}
|
|
err = writer.Close()
|
|
if err != nil {
|
|
logger.Error(err, "closing the writer")
|
|
}
|
|
}
|
|
|
|
func logRequestHandler(logger logr.Logger, h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger.Info("Incoming request", "method", r.Method, "uri", r.URL.String(),
|
|
"referer", r.Header.Get("Referer"), "userAgent", r.Header.Get("User-Agent"))
|
|
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|