mirror of
https://github.com/hanzokms/cli.git
synced 2026-10-30 20:30:00 +00:00
Two call sites, two different reasons they were wrong. `bootstrap --output k8-secret` dialled the cluster and created-or-updated a corev1.Secret from a Go template. Nothing invokes it — zero references across hanzo/kms, hanzo/kms-operator and hanzo/universe — and writing per-service Secrets is the kms-operator's job, not a CLI's. Deleted, along with --output and the three --k8-secret-* flags: one legal output value is not a choice. `pam kubernetes access` never dialled a cluster at all. It used clientcmd to splice a cluster/user/context into the operator's own ~/.kube/config, flip current-context, and revert on shutdown. It now writes a self-contained session kubeconfig (0600, removed on shutdown) and prints KUBECONFIG=<path>. That is a net code decrease — the whole revert block goes — and it closes the three TODOs the original left admitting it took no lock: a SIGKILLed proxy or two concurrent sessions could leave the real kubeconfig pointing at a dead port. It is never read or written now. Verified by rendering one and running kubectl config view --minify against it, which resolves only if current-context, cluster and user cross-reference. go.mod loses every k8s.io/* entry (-30 go.mod, -65 go.sum). Co-authored-by: Hanzo Dev <[email protected]>
277 lines
8.5 KiB
Go
277 lines
8.5 KiB
Go
package gatewayv2
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
func buildHttpInternalServerError(message string) string {
|
|
return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message)
|
|
}
|
|
|
|
func handleHTTPProxy(ctx context.Context, conn *tls.Conn, reader *bufio.Reader, forwardConfig *ForwardConfig) error {
|
|
targetURL := fmt.Sprintf("%s:%d", forwardConfig.TargetHost, forwardConfig.TargetPort)
|
|
caCert := forwardConfig.CACertificate
|
|
verifyTLS := forwardConfig.VerifyTLS
|
|
|
|
transport := &http.Transport{
|
|
DisableKeepAlives: false,
|
|
MaxIdleConns: 10,
|
|
IdleConnTimeout: 30 * time.Second,
|
|
}
|
|
|
|
if strings.HasPrefix(targetURL, "https://") {
|
|
tlsConfig := &tls.Config{}
|
|
|
|
if len(caCert) > 0 {
|
|
caCertPool := x509.NewCertPool()
|
|
if caCertPool.AppendCertsFromPEM(caCert) {
|
|
tlsConfig.RootCAs = caCertPool
|
|
log.Info().Msg("Using provided CA certificate from gateway client")
|
|
} else {
|
|
log.Error().Msg("Failed to parse provided CA certificate")
|
|
}
|
|
}
|
|
|
|
tlsConfig.InsecureSkipVerify = !verifyTLS
|
|
log.Info().Msgf("TLS verification set to: %v", verifyTLS)
|
|
|
|
transport.TLSClientConfig = tlsConfig
|
|
}
|
|
|
|
// Loop to handle multiple HTTP requests on the same connection
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Info().Msg("Context cancelled, closing HTTP proxy connection")
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
log.Info().Msg("Attempting to read HTTP request...")
|
|
|
|
// Create a channel to receive the request or error
|
|
reqCh := make(chan *http.Request, 1)
|
|
errCh := make(chan error, 1)
|
|
|
|
// Read request in a goroutine so we can cancel it
|
|
go func() {
|
|
req, err := http.ReadRequest(reader)
|
|
if err != nil {
|
|
errCh <- err
|
|
} else {
|
|
reqCh <- req
|
|
}
|
|
}()
|
|
|
|
var req *http.Request
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Info().Msg("Context cancelled while reading HTTP request")
|
|
return ctx.Err()
|
|
case err := <-errCh:
|
|
if errors.Is(err, io.EOF) {
|
|
log.Info().Msg("Client closed HTTP connection")
|
|
return nil
|
|
}
|
|
log.Error().Msgf("Failed to read HTTP request: %v", err)
|
|
return fmt.Errorf("failed to read HTTP request: %v", err)
|
|
case req = <-reqCh:
|
|
// Successfully received request
|
|
}
|
|
|
|
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
|
|
|
|
actionHeader := HttpProxyAction(req.Header.Get(KMS_HTTP_PROXY_ACTION_HEADER))
|
|
|
|
// Only platform actor can perform privileged actions
|
|
if actionHeader != "" && forwardConfig.ActorType == ActorTypePlatform {
|
|
if actionHeader == HttpProxyActionInjectGatewayK8sServiceAccountToken {
|
|
token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
|
|
if err != nil {
|
|
conn.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
|
continue // Continue to next request instead of returning
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
|
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
|
} else if actionHeader == HttpProxyActionUseGatewayK8sServiceAccount {
|
|
// will work without a target URL set
|
|
// set the ca cert to the pod's k8s service account ca cert:
|
|
caCert, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH)
|
|
if err != nil {
|
|
conn.Write([]byte(buildHttpInternalServerError("failed to read k8s sa ca cert")))
|
|
continue
|
|
}
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
|
conn.Write([]byte(buildHttpInternalServerError("failed to parse k8s sa ca cert")))
|
|
continue
|
|
}
|
|
|
|
transport.TLSClientConfig = &tls.Config{
|
|
RootCAs: caCertPool,
|
|
}
|
|
|
|
// set authorization header to the pod's k8s service account token:
|
|
token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
|
|
if err != nil {
|
|
conn.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
|
continue
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
|
|
|
// update the target URL to point to the kubernetes API server:
|
|
kubernetesServiceHost := os.Getenv(KUBERNETES_SERVICE_HOST_ENV_NAME)
|
|
kubernetesServicePort := os.Getenv(KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME)
|
|
|
|
fullBaseUrl := fmt.Sprintf("https://%s:%s", kubernetesServiceHost, kubernetesServicePort)
|
|
targetURL = fullBaseUrl
|
|
|
|
log.Info().Msgf("Redirected request to Kubernetes API server: %s", targetURL)
|
|
}
|
|
|
|
req.Header.Del(KMS_HTTP_PROXY_ACTION_HEADER)
|
|
}
|
|
|
|
// Build full target URL
|
|
var targetFullURL string
|
|
if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") {
|
|
baseURL := strings.TrimSuffix(targetURL, "/")
|
|
targetFullURL = baseURL + req.URL.Path
|
|
if req.URL.RawQuery != "" {
|
|
targetFullURL += "?" + req.URL.RawQuery
|
|
}
|
|
} else {
|
|
baseURL := strings.TrimSuffix("http://"+targetURL, "/")
|
|
targetFullURL = baseURL + req.URL.Path
|
|
if req.URL.RawQuery != "" {
|
|
targetFullURL += "?" + req.URL.RawQuery
|
|
}
|
|
}
|
|
|
|
// create the request to the target
|
|
proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body)
|
|
if err != nil {
|
|
log.Error().Msgf("Failed to create proxy request: %v", err)
|
|
conn.Write([]byte(buildHttpInternalServerError("failed to create proxy request")))
|
|
continue // Continue to next request
|
|
}
|
|
proxyReq.Header = req.Header.Clone()
|
|
|
|
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
|
|
|
client := &http.Client{
|
|
Transport: transport,
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Do(proxyReq)
|
|
if err != nil {
|
|
log.Error().Msgf("Failed to reach target: %v", err)
|
|
conn.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error()))))
|
|
continue // Continue to next request
|
|
}
|
|
|
|
// Write the entire response (status line, headers, body) to the connection
|
|
resp.Header.Del("Connection")
|
|
|
|
log.Info().Msgf("Writing response to connection: %s", resp.Status)
|
|
|
|
if err := resp.Write(conn); err != nil {
|
|
log.Error().Err(err).Msg("Failed to write response to connection")
|
|
resp.Body.Close()
|
|
return fmt.Errorf("failed to write response to connection: %w", err)
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
// Check if client wants to close connection
|
|
if req.Header.Get("Connection") == "close" {
|
|
log.Info().Msg("Client requested connection close")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleTCPProxy(ctx context.Context, conn *tls.Conn, forwardConfig *ForwardConfig) error {
|
|
target := net.JoinHostPort(forwardConfig.TargetHost, strconv.Itoa(forwardConfig.TargetPort))
|
|
localConn, err := net.Dial("tcp", target)
|
|
if err != nil {
|
|
log.Error().Msgf("Failed to connect to local service %s: %v", target, err)
|
|
return fmt.Errorf("failed to connect to local service %s: %v", target, err)
|
|
}
|
|
defer localConn.Close()
|
|
|
|
log.Info().
|
|
Str("target", target).
|
|
Msg("TCP proxy connection established to local service")
|
|
|
|
// Create a context for this connection that gets cancelled when the parent context is cancelled
|
|
// or when either connection closes
|
|
connCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Error channel to collect errors from both copy goroutines
|
|
errCh := make(chan error, 2)
|
|
|
|
// Forward data from TLS connection to local service
|
|
go func() {
|
|
defer cancel()
|
|
bytesCopied, err := io.Copy(localConn, conn)
|
|
log.Info().Int64("bytes", bytesCopied).Msg("Copied from client to local service")
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
|
log.Debug().Msgf("TLS to local copy ended normally: %v", err)
|
|
} else {
|
|
log.Error().Msgf("TLS to local copy failed: %v", err)
|
|
}
|
|
}
|
|
errCh <- err
|
|
}()
|
|
|
|
// Forward data from local service to TLS connection
|
|
go func() {
|
|
defer cancel()
|
|
bytesCopied, err := io.Copy(conn, localConn)
|
|
log.Info().Int64("bytes", bytesCopied).Msg("Copied from local service to client")
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
|
log.Debug().Msgf("Local to TLS copy ended normally: %v", err)
|
|
} else {
|
|
log.Error().Msgf("Local to TLS copy failed: %v", err)
|
|
}
|
|
}
|
|
errCh <- err
|
|
}()
|
|
|
|
// Wait for either context cancellation or one of the copy operations to complete
|
|
select {
|
|
case <-connCtx.Done():
|
|
log.Info().Msg("TCP proxy connection cancelled")
|
|
return connCtx.Err()
|
|
case err := <-errCh:
|
|
// One of the copy operations completed (or failed)
|
|
// The defer cancel() will stop the other goroutine
|
|
return err
|
|
}
|
|
}
|
|
|
|
func handlePing(ctx context.Context, conn *tls.Conn, reader *bufio.Reader) error {
|
|
conn.Write([]byte("PONG\n"))
|
|
return nil
|
|
}
|