diff --git a/kubernetes/tconfigd/config.yaml b/kubernetes/tconfigd/config.yaml new file mode 100644 index 0000000..3e57dbc --- /dev/null +++ b/kubernetes/tconfigd/config.yaml @@ -0,0 +1 @@ +enableTratInterception: "false" diff --git a/kubernetes/tconfigd/deploy.sh b/kubernetes/tconfigd/deploy.sh index 1c2f1c5..85e7194 100755 --- a/kubernetes/tconfigd/deploy.sh +++ b/kubernetes/tconfigd/deploy.sh @@ -1,11 +1,15 @@ docker build -t tconfigd:latest -f ../../service/Dockerfile ../../service/ kubectl create namespace tratteria + +kubectl create configmap config --from-file=config.yaml=config.yaml -n tratteria + +cd ../../rules +chmod +x deploy-rules.sh +./deploy-rules.sh example-rules +cd ../kubernetes/tconfigd + kubectl apply -f service-account.yaml kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl apply -f tratteria-agent-injector-mutating-webhook.yaml - -cd ../../rules -chmod +x deploy-rules.sh -./deploy-rules.sh example-rules \ No newline at end of file diff --git a/kubernetes/tconfigd/deployment.yaml b/kubernetes/tconfigd/deployment.yaml index 87428ea..e293541 100644 --- a/kubernetes/tconfigd/deployment.yaml +++ b/kubernetes/tconfigd/deployment.yaml @@ -24,6 +24,7 @@ spec: value: spiffe://tratteria.io/tratteria image: tconfigd name: tconfigd + args: ["/etc/tconfigd/config/config.yaml"] imagePullPolicy: Never ports: - containerPort: 9060 @@ -31,20 +32,26 @@ spec: - containerPort: 443 protocol: TCP volumeMounts: - - name: config-volume - mountPath: "/etc/rules" + - name: trats-rules-volume + mountPath: "/etc/tconfigd/rules" + readOnly: true + - name: tconfigd-config-volume + mountPath: "/etc/tconfigd/config" readOnly: true - mountPath: /run/spire/sockets name: spire-agent-socket readOnly: true restartPolicy: Always volumes: - - name: config-volume + - name: trats-rules-volume configMap: name: trats-rules-config items: - key: "trats-rules.ndjson" path: "trats-rules.ndjson" + - name: tconfigd-config-volume + configMap: + name: config - name: spire-agent-socket hostPath: path: /run/spire/sockets diff --git a/service/api/pkg/rules/rules.go b/service/api/pkg/rules/rules.go index 26bfc4b..5f891d4 100644 --- a/service/api/pkg/rules/rules.go +++ b/service/api/pkg/rules/rules.go @@ -19,7 +19,7 @@ func NewRules() *Rules { } func (r *Rules) Load() error { - traTs, generationRules, verificationRules, err := parse("/etc/rules/trats-rules.ndjson") + traTs, generationRules, verificationRules, err := parse("/etc/tconfigd/rules/trats-rules.ndjson") if err != nil { return err } diff --git a/service/api/setup.go b/service/api/setup.go index 89c029a..6ffd415 100644 --- a/service/api/setup.go +++ b/service/api/setup.go @@ -59,7 +59,7 @@ func Run() error { ReadTimeout: 15 * time.Second, } - logger.Info("Starting rules server on port 9060.") + logger.Info("Starting api server on port 9060.") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("Failed to start the api server", zap.Error(err)) diff --git a/service/cmd/main.go b/service/cmd/main.go index 8c25d10..ea722cd 100644 --- a/service/cmd/main.go +++ b/service/cmd/main.go @@ -8,7 +8,9 @@ import ( "syscall" "github.com/tratteria/tconfigd/api" + "github.com/tratteria/tconfigd/config" "github.com/tratteria/tconfigd/webhook" + "github.com/tratteria/tconfigd/webhook/webhookconfig" ) func main() { @@ -17,6 +19,17 @@ func main() { setupSignalHandler(cancel) + if len(os.Args) < 2 { + log.Fatalf("No configuration file provided. Please specify the configuration path as an argument when running the service.\nUsage: %s ", os.Args[0]) + } + + configPath := os.Args[1] + + appConfig, err := config.GetAppConfig(configPath) + if err != nil { + log.Fatalf("Error reading configuration: %v", err) + } + go func() { log.Println("Starting API server...") @@ -28,7 +41,9 @@ func main() { go func() { log.Println("Starting Webhook server...") - if err := webhook.Run(); err != nil { + webhookConfig := webhookconfig.WebhookConfig{EnableTratInterception: bool(appConfig.EnableTratInterception)} + + if err := webhook.Run(&webhookConfig); err != nil { log.Fatalf("Webhook server failed: %v", err) } }() diff --git a/service/config/config.go b/service/config/config.go new file mode 100644 index 0000000..820c5fc --- /dev/null +++ b/service/config/config.go @@ -0,0 +1,121 @@ +package config + +import ( + "fmt" + "os" + "reflect" + "regexp" + "strconv" + + "gopkg.in/yaml.v2" +) + +type BoolFromString bool + +func (b *BoolFromString) UnmarshalYAML(unmarshal func(interface{}) error) error { + var tmp interface{} + if err := unmarshal(&tmp); err != nil { + return err + } + + switch value := tmp.(type) { + case bool: + *b = BoolFromString(value) + case string: + if matched, envVarName := extractEnvVarName(value); matched { + envValue, err := getEnvVarValue(envVarName) + if err != nil { + return err + } + + boolVal, err := strconv.ParseBool(envValue) + + if err != nil { + return fmt.Errorf("error parsing boolean from environment variable: %v", err) + } + + *b = BoolFromString(boolVal) + } else { + boolVal, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("error parsing boolean from string: %v", err) + } + + *b = BoolFromString(boolVal) + } + default: + return fmt.Errorf("invalid type for a bool variable, expected bool or string, got %T", tmp) + } + + return nil +} + +type AppConfig struct { + EnableTratInterception BoolFromString `yaml:"enableTratInterception"` +} + +func GetAppConfig(configPath string) (*AppConfig, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var cfg AppConfig + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML configuration: %w", err) + } + + resolveEnvVariables(&cfg) + + return &cfg, nil +} + +func extractEnvVarName(s string) (bool, string) { + envVarRegex := regexp.MustCompile(`^\$\{([^}]+)\}$`) + matches := envVarRegex.FindStringSubmatch(s) + + if len(matches) > 1 { + return true, matches[1] + } + + return false, "" +} + +func getEnvVarValue(envVarName string) (string, error) { + if envValue, exists := os.LookupEnv(envVarName); exists { + return envValue, nil + } + + return "", fmt.Errorf("environment variable %s not set", envVarName) +} + +func resolveEnvVariablesUtil(v reflect.Value) { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.Kind() == reflect.String { + fieldValue := field.String() + + if matched, envVarName := extractEnvVarName(fieldValue); matched { + envValue, err := getEnvVarValue(envVarName) + if err != nil { + panic(err.Error()) + } + + field.SetString(envValue) + } + } else if field.Kind() == reflect.Struct { + resolveEnvVariablesUtil(field) + } else if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct { + resolveEnvVariablesUtil(field.Elem()) + } + } +} + +func resolveEnvVariables(cfg *AppConfig) { + v := reflect.ValueOf(cfg) + resolveEnvVariablesUtil(v) +} diff --git a/service/go.mod b/service/go.mod index f603868..5c2ebe2 100644 --- a/service/go.mod +++ b/service/go.mod @@ -8,6 +8,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.2.0 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.62.1 + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.30.1 k8s.io/apimachinery v0.30.1 ) @@ -33,7 +34,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/service/webhook/handler/handler.go b/service/webhook/handler/handler.go index 4c67a62..0a90c6c 100644 --- a/service/webhook/handler/handler.go +++ b/service/webhook/handler/handler.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/tratteria/tconfigd/webhook/pkg/util" + "github.com/tratteria/tconfigd/webhook/webhookconfig" "go.uber.org/zap" @@ -14,29 +15,31 @@ import ( ) type Handlers struct { - Logger *zap.Logger + config *webhookconfig.WebhookConfig + logger *zap.Logger } -func NewHandlers(logger *zap.Logger) *Handlers { +func NewHandlers(config *webhookconfig.WebhookConfig, logger *zap.Logger) *Handlers { return &Handlers{ - Logger: logger, + config: config, + logger: logger, } } func (h *Handlers) InjectTratteriaAgent(w http.ResponseWriter, r *http.Request) { - h.Logger.Info("Received Agent Injection Request") + h.logger.Info("Received Agent Injection Request") var admissionReview admissionv1.AdmissionReview if err := json.NewDecoder(r.Body).Decode(&admissionReview); err != nil { - h.Logger.Error("Failed to decode admission review", zap.Error(err)) + h.logger.Error("Failed to decode admission review", zap.Error(err)) http.Error(w, "could not decode admission review", http.StatusBadRequest) return } if admissionReview.Request == nil { - h.Logger.Error("Received an AdmissionReview with no Request") + h.logger.Error("Received an AdmissionReview with no Request") http.Error(w, "received an AdmissionReview with no Request", http.StatusBadRequest) return @@ -49,15 +52,15 @@ func (h *Handlers) InjectTratteriaAgent(w http.ResponseWriter, r *http.Request) var pod corev1.Pod if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil { - h.Logger.Error("Could not unmarshal raw object into pod", zap.Error(err)) + h.logger.Error("Could not unmarshal raw object into pod", zap.Error(err)) admissionResponse.Result = &metav1.Status{ Message: err.Error(), } } else { - patchOps, err := util.CreatePodPatch(&pod) + patchOps, err := util.CreatePodPatch(&pod, h.config.EnableTratInterception) if err != nil { - h.Logger.Error("Could not create patch for pod", zap.Error(err)) + h.logger.Error("Could not create patch for pod", zap.Error(err)) admissionResponse.Result = &metav1.Status{ Message: err.Error(), } @@ -65,7 +68,7 @@ func (h *Handlers) InjectTratteriaAgent(w http.ResponseWriter, r *http.Request) patchBytes, err := json.Marshal(patchOps) if err != nil { - h.Logger.Error("Failed to marshal patch operations", zap.Error(err)) + h.logger.Error("Failed to marshal patch operations", zap.Error(err)) admissionResponse.Result = &metav1.Status{ Message: err.Error(), } @@ -89,9 +92,9 @@ func (h *Handlers) InjectTratteriaAgent(w http.ResponseWriter, r *http.Request) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(responseAdmissionReview); err != nil { - h.Logger.Error("Failed to write response", zap.Error(err)) + h.logger.Error("Failed to write response", zap.Error(err)) http.Error(w, "failed to write response", http.StatusInternalServerError) } - h.Logger.Info("Agent Injection Request Processed Successfully", zap.Any("patched-pod", responseAdmissionReview)) + h.logger.Info("Agent Injection Request Processed Successfully", zap.Any("patched-pod", responseAdmissionReview)) } diff --git a/service/webhook/pkg/util/util.go b/service/webhook/pkg/util/util.go index b98d030..098ba59 100644 --- a/service/webhook/pkg/util/util.go +++ b/service/webhook/pkg/util/util.go @@ -9,7 +9,7 @@ import ( corev1 "k8s.io/api/core/v1" ) -func CreatePodPatch(pod *corev1.Pod) ([]jsonpatch.JsonPatchOperation, error) { +func CreatePodPatch(pod *corev1.Pod, injectInitContainer bool) ([]jsonpatch.JsonPatchOperation, error) { var patch []jsonpatch.JsonPatchOperation shouldInject, ok := pod.Annotations["tratteria/inject-sidecar"] @@ -28,35 +28,37 @@ func CreatePodPatch(pod *corev1.Pod) ([]jsonpatch.JsonPatchOperation, error) { return nil, fmt.Errorf("service-port must be a valid number") } - initContainer := corev1.Container{ - Name: "tratteria-agent-init", - Image: "tratteria-agent-init:latest", - Args: []string{"-i", servicePort, "-p", "9070"}, - ImagePullPolicy: corev1.PullNever, - SecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"NET_ADMIN"}, + if injectInitContainer { + initContainer := corev1.Container{ + Name: "tratteria-agent-init", + Image: "tratteria-agent-init:latest", + Args: []string{"-i", servicePort, "-p", "9070"}, + ImagePullPolicy: corev1.PullNever, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, }, - }, - } + } - initContainerJson, err := json.Marshal(initContainer) - if err != nil { - return nil, err - } + initContainerJson, err := json.Marshal(initContainer) + if err != nil { + return nil, err + } - if pod.Spec.InitContainers == nil { - patch = append(patch, jsonpatch.JsonPatchOperation{ - Operation: "add", - Path: "/spec/initContainers", - Value: []json.RawMessage{json.RawMessage(initContainerJson)}, - }) - } else { - patch = append(patch, jsonpatch.JsonPatchOperation{ - Operation: "add", - Path: "/spec/initContainers/-", - Value: json.RawMessage(initContainerJson), - }) + if pod.Spec.InitContainers == nil { + patch = append(patch, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: "/spec/initContainers", + Value: []json.RawMessage{json.RawMessage(initContainerJson)}, + }) + } else { + patch = append(patch, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: "/spec/initContainers/-", + Value: json.RawMessage(initContainerJson), + }) + } } sidecar := corev1.Container{ diff --git a/service/webhook/setup.go b/service/webhook/setup.go index 0762e0e..64e0d0f 100644 --- a/service/webhook/setup.go +++ b/service/webhook/setup.go @@ -13,6 +13,7 @@ import ( "github.com/tratteria/tconfigd/webhook/handler" "github.com/tratteria/tconfigd/webhook/pkg/tlscreds" + "github.com/tratteria/tconfigd/webhook/webhookconfig" ) type Webhook struct { @@ -22,7 +23,7 @@ type Webhook struct { Logger *zap.Logger } -func Run() error { +func Run(config *webhookconfig.WebhookConfig) error { logger, err := zap.NewProduction() if err != nil { return fmt.Errorf("cannot initialize Zap logger: %w", err) @@ -34,7 +35,7 @@ func Run() error { } }() - handler := handler.NewHandlers(logger) + handler := handler.NewHandlers(config, logger) webhook := &Webhook{ Router: mux.NewRouter(), diff --git a/service/webhook/webhookconfig/webhookconfig.go b/service/webhook/webhookconfig/webhookconfig.go new file mode 100644 index 0000000..7dca0cb --- /dev/null +++ b/service/webhook/webhookconfig/webhookconfig.go @@ -0,0 +1,5 @@ +package webhookconfig + +type WebhookConfig struct { + EnableTratInterception bool +}