diff --git a/api/v1alpha1/ctlog_types.go b/api/v1alpha1/ctlog_types.go index 5c644bcd7..137afd76b 100644 --- a/api/v1alpha1/ctlog_types.go +++ b/api/v1alpha1/ctlog_types.go @@ -48,6 +48,10 @@ type CTlogSpec struct { // publicKeyRef, rootCertificates and trillian will be overridden. //+optional ServerConfigRef *LocalObjectReference `json:"serverConfigRef,omitempty"` + + // ConfigMap with additional bundle of trusted CA + //+optional + TrustedCA *LocalObjectReference `json:"trustedCA,omitempty"` } // CTlogStatus defines the observed state of CTlog component @@ -57,6 +61,7 @@ type CTlogStatus struct { PrivateKeyPasswordRef *SecretKeySelector `json:"privateKeyPasswordRef,omitempty"` PublicKeyRef *SecretKeySelector `json:"publicKeyRef,omitempty"` RootCertificates []SecretKeySelector `json:"rootCertificates,omitempty"` + TrustedCA *LocalObjectReference `json:"trustedCA,omitempty"` // The ID of a Trillian tree that stores the log data. // +kubebuilder:validation:Type=number TreeID *int64 `json:"treeID,omitempty"` diff --git a/api/v1alpha1/rekor_types.go b/api/v1alpha1/rekor_types.go index 812a6888e..5e2804f31 100644 --- a/api/v1alpha1/rekor_types.go +++ b/api/v1alpha1/rekor_types.go @@ -39,6 +39,9 @@ type RekorSpec struct { // +patchMergeKey=treeID // +kubebuilder:default:={} Sharding []RekorLogRange `json:"sharding,omitempty"` + // ConfigMap with additional bundle of trusted CA + //+optional + TrustedCA *LocalObjectReference `json:"trustedCA,omitempty"` } type RekorSigner struct { diff --git a/api/v1alpha1/trillian_types.go b/api/v1alpha1/trillian_types.go index 838504433..29ce2cd66 100644 --- a/api/v1alpha1/trillian_types.go +++ b/api/v1alpha1/trillian_types.go @@ -27,6 +27,8 @@ type TrillianSpec struct { //+kubebuilder:validation:XValidation:rule=((!self.create && self.databaseSecretRef != null) || self.create),message=databaseSecretRef cannot be empty //+kubebuilder:default:={create: true, pvc: {size: "5Gi", retain: true, accessModes: {ReadWriteOnce}}} Db TrillianDB `json:"database,omitempty"` + //+optional + Server TrillianServer `json:"server,omitempty"` // Enable Monitoring for Logsigner and Logserver Monitoring MonitoringConfig `json:"monitoring,omitempty"` // ConfigMap with additional bundle of trusted CA @@ -55,9 +57,16 @@ type TrillianDB struct { TLS TLS `json:"tls,omitempty"` } +type TrillianServer struct { + // Configuration for enabling TLS (Transport Layer Security) encryption for Trillian server. + //+optional + TLS TLS `json:"tls,omitempty"` +} + // TrillianStatus defines the observed state of Trillian type TrillianStatus struct { - Db TrillianDB `json:"database,omitempty"` + Db TrillianDB `json:"database,omitempty"` + Server TrillianServer `json:"server,omitempty"` // +listType=map // +listMapKey=type // +patchStrategy=merge diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8d1642b6f..6f7f56b6a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -167,6 +167,11 @@ func (in *CTlogSpec) DeepCopyInto(out *CTlogSpec) { *out = new(LocalObjectReference) **out = **in } + if in.TrustedCA != nil { + in, out := &in.TrustedCA, &out.TrustedCA + *out = new(LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CTlogSpec. @@ -207,6 +212,11 @@ func (in *CTlogStatus) DeepCopyInto(out *CTlogStatus) { *out = make([]SecretKeySelector, len(*in)) copy(*out, *in) } + if in.TrustedCA != nil { + in, out := &in.TrustedCA, &out.TrustedCA + *out = new(LocalObjectReference) + **out = **in + } if in.TreeID != nil { in, out := &in.TreeID, &out.TreeID *out = new(int64) @@ -784,6 +794,11 @@ func (in *RekorSpec) DeepCopyInto(out *RekorSpec) { *out = make([]RekorLogRange, len(*in)) copy(*out, *in) } + if in.TrustedCA != nil { + in, out := &in.TrustedCA, &out.TrustedCA + *out = new(LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RekorSpec. @@ -1302,6 +1317,22 @@ func (in *TrillianList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrillianServer) DeepCopyInto(out *TrillianServer) { + *out = *in + in.TLS.DeepCopyInto(&out.TLS) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrillianServer. +func (in *TrillianServer) DeepCopy() *TrillianServer { + if in == nil { + return nil + } + out := new(TrillianServer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TrillianService) DeepCopyInto(out *TrillianService) { *out = *in @@ -1326,6 +1357,7 @@ func (in *TrillianService) DeepCopy() *TrillianService { func (in *TrillianSpec) DeepCopyInto(out *TrillianSpec) { *out = *in in.Db.DeepCopyInto(&out.Db) + in.Server.DeepCopyInto(&out.Server) out.Monitoring = in.Monitoring if in.TrustedCA != nil { in, out := &in.TrustedCA, &out.TrustedCA @@ -1348,6 +1380,7 @@ func (in *TrillianSpec) DeepCopy() *TrillianSpec { func (in *TrillianStatus) DeepCopyInto(out *TrillianStatus) { *out = *in in.Db.DeepCopyInto(&out.Db) + in.Server.DeepCopyInto(&out.Server) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) diff --git a/bundle/manifests/rhtas.redhat.com_ctlogs.yaml b/bundle/manifests/rhtas.redhat.com_ctlogs.yaml index 1d4802c09..01276f8ba 100644 --- a/bundle/manifests/rhtas.redhat.com_ctlogs.yaml +++ b/bundle/manifests/rhtas.redhat.com_ctlogs.yaml @@ -174,6 +174,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object x-kubernetes-validations: - message: privateKeyRef cannot be empty @@ -347,6 +359,20 @@ spec: description: The ID of a Trillian tree that stores the log data. format: int64 type: number + trustedCA: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object type: object served: true diff --git a/bundle/manifests/rhtas.redhat.com_rekors.yaml b/bundle/manifests/rhtas.redhat.com_rekors.yaml index 7f2606baa..968fbe929 100644 --- a/bundle/manifests/rhtas.redhat.com_rekors.yaml +++ b/bundle/manifests/rhtas.redhat.com_rekors.yaml @@ -278,6 +278,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object status: description: RekorStatus defines the observed state of Rekor diff --git a/bundle/manifests/rhtas.redhat.com_securesigns.yaml b/bundle/manifests/rhtas.redhat.com_securesigns.yaml index 1eea0a2dd..f403d79e1 100644 --- a/bundle/manifests/rhtas.redhat.com_securesigns.yaml +++ b/bundle/manifests/rhtas.redhat.com_securesigns.yaml @@ -190,6 +190,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object x-kubernetes-validations: - message: privateKeyRef cannot be empty @@ -686,6 +698,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object trillian: description: TrillianSpec defines the desired state of Trillian @@ -842,6 +866,55 @@ spec: required: - enabled type: object + server: + properties: + tls: + description: Configuration for enabling TLS (Transport Layer + Security) encryption for Trillian server. + properties: + certificateRef: + description: Reference to the certificate secret used + for TLS encryption. + properties: + key: + description: The key of the secret to select from. + Must be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used + for TLS encryption. + properties: + key: + description: The key of the secret to select from. + Must be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) + type: object trustedCA: description: ConfigMap with additional bundle of trusted CA properties: diff --git a/bundle/manifests/rhtas.redhat.com_trillians.yaml b/bundle/manifests/rhtas.redhat.com_trillians.yaml index 98ac8c36c..cde495792 100644 --- a/bundle/manifests/rhtas.redhat.com_trillians.yaml +++ b/bundle/manifests/rhtas.redhat.com_trillians.yaml @@ -195,6 +195,55 @@ spec: required: - enabled type: object + server: + properties: + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for Trillian server. + properties: + certificateRef: + description: Reference to the certificate secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) + type: object trustedCA: description: ConfigMap with additional bundle of trusted CA properties: @@ -410,6 +459,55 @@ spec: required: - create type: object + server: + properties: + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for Trillian server. + properties: + certificateRef: + description: Reference to the certificate secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) + type: object type: object type: object served: true diff --git a/cmd/main.go b/cmd/main.go index 9cb9e4c76..1875c3014 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -122,6 +122,7 @@ func main() { utils.StringFlagOrEnv(&constants.SegmentBackupImage, "segment-backup-job-image", "SEGMENT_BACKUP_JOB_IMAGE", constants.SegmentBackupImage, "The image used for the segment backup job") flag.StringVar(&clidownload.CliHostName, "cli-server-hostname", "", "The hostname for the cli server") utils.StringFlagOrEnv(&constants.TimestampAuthorityImage, "timestamp-authority-image", "TIMESTAMP_AUTHORITY_IMAGE", constants.TimestampAuthorityImage, "The image used for Timestamp Authority") + utils.StringFlagOrEnv(&constants.CreateTreeImage, "create-tree-image", "CREATE_TREE_IMAGE", constants.CreateTreeImage, "The image used for the Trillian create tree job") klog.InitFlags(flag.CommandLine) flag.Parse() diff --git a/config/crd/bases/rhtas.redhat.com_ctlogs.yaml b/config/crd/bases/rhtas.redhat.com_ctlogs.yaml index 798ed7369..605b4afbf 100644 --- a/config/crd/bases/rhtas.redhat.com_ctlogs.yaml +++ b/config/crd/bases/rhtas.redhat.com_ctlogs.yaml @@ -174,6 +174,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object x-kubernetes-validations: - message: privateKeyRef cannot be empty @@ -347,6 +359,20 @@ spec: description: The ID of a Trillian tree that stores the log data. format: int64 type: number + trustedCA: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object type: object served: true diff --git a/config/crd/bases/rhtas.redhat.com_rekors.yaml b/config/crd/bases/rhtas.redhat.com_rekors.yaml index d3eb90838..8f6baa5d8 100644 --- a/config/crd/bases/rhtas.redhat.com_rekors.yaml +++ b/config/crd/bases/rhtas.redhat.com_rekors.yaml @@ -278,6 +278,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object status: description: RekorStatus defines the observed state of Rekor diff --git a/config/crd/bases/rhtas.redhat.com_securesigns.yaml b/config/crd/bases/rhtas.redhat.com_securesigns.yaml index 16193a723..de88fea1e 100644 --- a/config/crd/bases/rhtas.redhat.com_securesigns.yaml +++ b/config/crd/bases/rhtas.redhat.com_securesigns.yaml @@ -190,6 +190,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object x-kubernetes-validations: - message: privateKeyRef cannot be empty @@ -686,6 +698,18 @@ spec: minimum: 1 type: integer type: object + trustedCA: + description: ConfigMap with additional bundle of trusted CA + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic type: object trillian: description: TrillianSpec defines the desired state of Trillian @@ -842,6 +866,55 @@ spec: required: - enabled type: object + server: + properties: + tls: + description: Configuration for enabling TLS (Transport Layer + Security) encryption for Trillian server. + properties: + certificateRef: + description: Reference to the certificate secret used + for TLS encryption. + properties: + key: + description: The key of the secret to select from. + Must be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used + for TLS encryption. + properties: + key: + description: The key of the secret to select from. + Must be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) + type: object trustedCA: description: ConfigMap with additional bundle of trusted CA properties: diff --git a/config/crd/bases/rhtas.redhat.com_trillians.yaml b/config/crd/bases/rhtas.redhat.com_trillians.yaml index cb1c67d28..a20306f13 100644 --- a/config/crd/bases/rhtas.redhat.com_trillians.yaml +++ b/config/crd/bases/rhtas.redhat.com_trillians.yaml @@ -195,6 +195,55 @@ spec: required: - enabled type: object + server: + properties: + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for Trillian server. + properties: + certificateRef: + description: Reference to the certificate secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) + type: object trustedCA: description: ConfigMap with additional bundle of trusted CA properties: @@ -410,6 +459,55 @@ spec: required: - create type: object + server: + properties: + tls: + description: Configuration for enabling TLS (Transport Layer Security) + encryption for Trillian server. + properties: + certificateRef: + description: Reference to the certificate secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + privateKeyRef: + description: Reference to the private key secret used for + TLS encryption. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: privateKeyRef cannot be empty + rule: (!has(self.certificateRef) || has(self.privateKeyRef)) + type: object type: object type: object served: true diff --git a/internal/controller/common/action/transitions/create_tree_job.go b/internal/controller/common/action/transitions/create_tree_job.go new file mode 100644 index 000000000..466bab675 --- /dev/null +++ b/internal/controller/common/action/transitions/create_tree_job.go @@ -0,0 +1,210 @@ +package transitions + +import ( + "context" + "fmt" + + "github.com/securesign/operator/internal/apis" + "github.com/securesign/operator/internal/controller/common/action" + cutils "github.com/securesign/operator/internal/controller/common/utils" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes/job" + "github.com/securesign/operator/internal/controller/constants" + "github.com/securesign/operator/internal/controller/ctlog/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type TreeJobSupplier[T apis.ConditionsAwareObject] func( + instance T, +) ( + trillianAddress string, + instanceName string, + treeJobConfigMapName string, + treeJobName string, + treeDisplayName string, + trillianDeploymentName string, + namespace string, + trillianPort *int32, + caPath string, + rbac string, + labels map[string]string, + annotations map[string]string, + treeID *int64, + err error, +) + +func NewCreateTreeJobAction[T apis.ConditionsAwareObject](supplier TreeJobSupplier[T]) action.Action[T] { + return &createTreeJobAction[T]{supplier: supplier} +} + +type createTreeJobAction[T apis.ConditionsAwareObject] struct { + action.BaseAction + supplier TreeJobSupplier[T] +} + +func (i createTreeJobAction[T]) Name() string { + return "create tree job" +} + +func (i createTreeJobAction[T]) CanHandle(ctx context.Context, instance T) bool { + _, _, treeJobConfigMapName, _, _, _, _, _, _, _, _, _, treeID, err := i.supplier(instance) + if err != nil { + return false + } + cm, _ := kubernetes.GetConfigMap(ctx, i.Client, instance.GetNamespace(), treeJobConfigMapName) + c := meta.FindStatusCondition(instance.GetConditions(), constants.Ready) + return (c.Reason == constants.Creating || c.Reason == constants.Ready) && cm == nil && treeID == nil +} + +func (i createTreeJobAction[T]) Handle(ctx context.Context, instance T) *action.Result { + trillianAddress, instanceName, treeJobConfigMapName, treeJobName, treeDisplayName, trillianDeploymentName, namespace, trillianPort, caPath, rbac, labels, annotations, _, err := i.supplier(instance) + + if err != nil { + return i.Failed(fmt.Errorf("failed to get job details: %w", err)) + } + + var trillUrl string + + switch { + case trillianPort == nil: + err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) + case trillianAddress == "": + trillUrl = fmt.Sprintf("%s.%s.svc:%d", trillianDeploymentName, namespace, *trillianPort) + default: + trillUrl = fmt.Sprintf("%s:%d", trillianAddress, *trillianPort) + } + if err != nil { + return i.Failed(err) + } + + i.Logger.V(1).Info("trillian logserver", "address", trillUrl) + + instance.SetCondition(metav1.Condition{ + Type: treeJobName, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: "Creating tree Job", + }) + + // Needed for configMap clean-up + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: treeJobConfigMapName, + Namespace: instance.GetNamespace(), + Labels: labels, + }, + Data: map[string]string{}, + } + + if err := controllerutil.SetControllerReference(instance, configMap, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for configMap: %w", err)) + } + + updated, err := i.Ensure(ctx, configMap) + if err != nil { + instance.SetCondition(metav1.Condition{ + Type: treeJobName, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.Failed(err) + } + if updated { + instance.SetCondition(metav1.Condition{ + Type: treeJobName, + Status: metav1.ConditionFalse, + Reason: constants.Creating, + Message: "ConfigMap created", + }) + i.Recorder.Event(instance, corev1.EventTypeNormal, "TreeConfigCreated", "ConfigMap for TreeID created") + } + + parallelism := int32(1) + completions := int32(1) + activeDeadlineSeconds := int64(600) + backoffLimit := int32(5) + + trustedCAAnnotation := cutils.TrustedCAAnnotationToReference(annotations) + + cmd := "" + switch { + case trustedCAAnnotation != nil: + cmd = fmt.Sprintf("/createtree --admin_server=%s --display_name=%s --tls_cert_file=%s", trillUrl, treeDisplayName, caPath) + case kubernetes.IsOpenShift(): + cmd = fmt.Sprintf("/createtree --admin_server=%s --display_name=%s --tls_cert_file=/var/run/secrets/tas/tls.crt", trillUrl, treeDisplayName) + default: + cmd = fmt.Sprintf("/createtree --admin_server=%s --display_name=%s", trillUrl, treeDisplayName) + } + command := []string{ + "/bin/sh", + "-c", + fmt.Sprintf(` + TREE_ID=$(%s) + if [ $? -eq 0 ]; then + echo "TREE_ID=$TREE_ID" + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + API_SERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT} + curl -k -X PATCH $API_SERVER/api/v1/namespaces/$NAMESPACE/configmaps/"%s" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/merge-patch+json" \ + -d '{ + "data": { + "tree_id": "'$TREE_ID'" + } + }' + if [ $? -ne 0 ]; then + echo "Failed to update ConfigMap" >&2 + exit 1 + fi + else + echo "Failed to create tree" >&2 + exit 1 + fi + `, cmd, treeJobConfigMapName), + } + env := []corev1.EnvVar{} + + job := job.CreateJob(namespace, treeJobName, labels, constants.CreateTreeImage, rbac, parallelism, completions, activeDeadlineSeconds, backoffLimit, command, env) + if err := ctrl.SetControllerReference(instance, job, i.Client.Scheme()); err != nil { + return i.Failed(fmt.Errorf("could not set controller reference for job: %w", err)) + } + + if err := cutils.SetTrustedCA(&job.Spec.Template, trustedCAAnnotation); err != nil { + return i.Failed(err) + } + + if kubernetes.IsOpenShift() && trustedCAAnnotation == nil { + job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: "tls-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: instanceName + "-trillian-server-tls", + }, + }, + }) + job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ + Name: "tls-cert", + MountPath: "/var/run/secrets/tas", + ReadOnly: true, + }) + } + + if _, err := i.Ensure(ctx, job); err != nil { + return i.Failed(fmt.Errorf("failed to ensure the job: %w", err)) + } + + instance.SetCondition(metav1.Condition{ + Type: treeJobName, + Status: metav1.ConditionTrue, + Reason: constants.Creating, + Message: "Tree Job created", + }) + + return i.Continue() +} diff --git a/internal/controller/common/create_tree.go b/internal/controller/common/create_tree.go deleted file mode 100644 index 0a60e0f50..000000000 --- a/internal/controller/common/create_tree.go +++ /dev/null @@ -1,98 +0,0 @@ -package common - -import ( - "context" - "fmt" - "net" - "time" - - "github.com/google/trillian" - "github.com/google/trillian/client" - "github.com/securesign/operator/internal/controller/common/utils/kubernetes" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/protobuf/types/known/durationpb" - "k8s.io/klog/v2" -) - -// reference code https://github.com/sigstore/scaffolding/blob/main/cmd/trillian/createtree/main.go -func CreateTrillianTree(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) { - var err error - inContainer, err := kubernetes.ContainerMode() - if err == nil { - if !inContainer { - fmt.Println("Operator is running on localhost. You need to port-forward services.") - for it := 0; it < 60; it++ { - if rawConnect("localhost", "8091") { - fmt.Println("Connection is open.") - trillianURL = "localhost:8091" - break - } else { - fmt.Println("Execute `oc port-forward service/trillian-logserver 8091 8091` in your namespace to continue.") - time.Sleep(time.Duration(5) * time.Second) - } - } - - } - } else { - klog.Info("Can't recognise operator mode - expecting in-container run") - } - req, err := newRequest(displayName) - if err != nil { - return nil, err - } - var opts grpc.DialOption - klog.Warning("Using an insecure gRPC connection to Trillian") - opts = grpc.WithTransportCredentials(insecure.NewCredentials()) - conn, err := grpc.Dial(trillianURL, opts) - if err != nil { - return nil, fmt.Errorf("failed to dial: %w", err) - } - defer func() { _ = conn.Close() }() - - adminClient := trillian.NewTrillianAdminClient(conn) - logClient := trillian.NewTrillianLogClient(conn) - - timeout := time.Duration(deadline) * time.Second - ctx2, cancel := context.WithTimeout(ctx, timeout) - tree, err := client.CreateAndInitTree(ctx2, req, adminClient, logClient) - defer cancel() - if err != nil { - return nil, fmt.Errorf("could not create Trillian tree: %w", err) - } - return tree, err -} - -func rawConnect(host string, port string) bool { - timeout := time.Second - conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) - if err != nil { - return false - } - if conn != nil { - defer func() { _ = conn.Close() }() - return true - } - return false -} - -func newRequest(displayName string) (*trillian.CreateTreeRequest, error) { - ts, ok := trillian.TreeState_value[trillian.TreeState_ACTIVE.String()] - if !ok { - return nil, fmt.Errorf("unknown TreeState: %v", trillian.TreeState_ACTIVE) - } - - tt, ok := trillian.TreeType_value[trillian.TreeType_LOG.String()] - if !ok { - return nil, fmt.Errorf("unknown TreeType: %v", trillian.TreeType_LOG) - } - - ctr := &trillian.CreateTreeRequest{Tree: &trillian.Tree{ - TreeState: trillian.TreeState(ts), - TreeType: trillian.TreeType(tt), - DisplayName: displayName, - MaxRootDuration: durationpb.New(time.Hour), - }} - - return ctr, nil -} diff --git a/internal/controller/constants/images.go b/internal/controller/constants/images.go index ba904613e..edd9a141b 100644 --- a/internal/controller/constants/images.go +++ b/internal/controller/constants/images.go @@ -25,4 +25,5 @@ var ( ClientServerImage_f = "registry.redhat.io/rhtas/client-server-f-rhel9@sha256:8c8c4bfcbc8728ee46a427a4179622e4437e3502aa4b29af7539bf2eee999ff6" SegmentBackupImage = "registry.redhat.io/rhtas/segment-reporting-rhel9@sha256:c7fa18f6dec1fdd308d5a6ed74f5f6bf2bd30d6759d7d2464875b6e80f269fb2" TimestampAuthorityImage = "registry.redhat.io/rhtas/timestamp-authority-rhel9@sha256:d957041e1f10faf087333b9f1d39b2bb4b26edd37a812192e67771c423950def" + CreateTreeImage = "registry.redhat.io/rhtas/trillian-createtree-rhel9@sha256:0a793e68b9398d73a47012cab0f9edf7b0b917060d59b4afdc9efc5e034595c8" ) diff --git a/internal/controller/ctlog/actions/constants.go b/internal/controller/ctlog/actions/constants.go index 5ead8d88d..c1ae0b302 100644 --- a/internal/controller/ctlog/actions/constants.go +++ b/internal/controller/ctlog/actions/constants.go @@ -8,13 +8,19 @@ const ( RBACName = "ctlog" MonitoringRoleName = "prometheus-k8s-ctlog" - CertCondition = "FulcioCertAvailable" - ServerPortName = "http" - ServerPort = 80 - ServerTargetPort = 6962 - MetricsPortName = "metrics" - MetricsPort = 6963 - ServerCondition = "ServerAvailable" + CertCondition = "FulcioCertAvailable" + ServerPortName = "http" + ServerPort = 80 + HttpsServerPortName = "https" + HttpsServerPort = 443 + ServerTargetPort = 6962 + MetricsPortName = "metrics" + MetricsPort = 6963 + ServerCondition = "ServerAvailable" + CtlogTreeName = "ctlog-tree" + CtlogTreeJobName = "ctlog-create-tree" + CtlogTreeJobCondition = "CtlogTreeJobAvailable" + CtlogTreeJobConfigMapName = "ctlog-tree-id-config" CTLPubLabel = constants.LabelNamespace + "/ctfe.pub" ) diff --git a/internal/controller/ctlog/actions/deployment.go b/internal/controller/ctlog/actions/deployment.go index ad6462398..32425b78d 100644 --- a/internal/controller/ctlog/actions/deployment.go +++ b/internal/controller/ctlog/actions/deployment.go @@ -46,7 +46,7 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) instance.Spec.Trillian.Address = fmt.Sprintf("%s.%s.svc", trillian.LogserverDeploymentName, instance.Namespace) } - dp, err := utils.CreateDeployment(instance, DeploymentName, RBACName, labels, ServerTargetPort, MetricsPort) + dp, err := utils.CreateDeployment(ctx, i.Client, instance, DeploymentName, RBACName, labels, ServerTargetPort, MetricsPort) if err != nil { meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: constants.Ready, @@ -56,7 +56,12 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) }) return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could create server Deployment: %w", err), instance) } - err = cutils.SetTrustedCA(&dp.Spec.Template, cutils.TrustedCAAnnotationToReference(instance.Annotations)) + caTrustRef := cutils.TrustedCAAnnotationToReference(instance.Annotations) + // override if spec.trustedCA is defined + if instance.Spec.TrustedCA != nil { + caTrustRef = instance.Spec.TrustedCA + } + err = cutils.SetTrustedCA(&dp.Spec.Template, caTrustRef) if err != nil { return i.Failed(err) } diff --git a/internal/controller/ctlog/actions/rbac.go b/internal/controller/ctlog/actions/rbac.go index b5d0028e3..cb097b96d 100644 --- a/internal/controller/ctlog/actions/rbac.go +++ b/internal/controller/ctlog/actions/rbac.go @@ -64,7 +64,7 @@ func (i rbacAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog) * { APIGroups: []string{""}, Resources: []string{"configmaps"}, - Verbs: []string{"create", "get", "update"}, + Verbs: []string{"create", "get", "update", "patch"}, }, { APIGroups: []string{""}, diff --git a/internal/controller/ctlog/actions/resolve_tree.go b/internal/controller/ctlog/actions/resolve_tree.go index 0c885551a..4e639c825 100644 --- a/internal/controller/ctlog/actions/resolve_tree.go +++ b/internal/controller/ctlog/actions/resolve_tree.go @@ -3,26 +3,21 @@ package actions import ( "context" "fmt" + "strconv" "github.com/google/trillian" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/ctlog/utils" - actions2 "github.com/securesign/operator/internal/controller/trillian/actions" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) -type createTree func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) - func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtasv1alpha1.CTlog] { - a := &resolveTreeAction{ - createTree: common.CreateTrillianTree, - } + a := &resolveTreeAction{} for _, opt := range opts { opt(a) @@ -32,7 +27,6 @@ func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtas type resolveTreeAction struct { action.BaseAction - createTree createTree } func (i resolveTreeAction) Name() string { @@ -62,23 +56,35 @@ func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.C } var err error var tree *trillian.Tree - var trillUrl string - switch { - case instance.Spec.Trillian.Port == nil: - err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) - case instance.Spec.Trillian.Address == "": - trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.Namespace, *instance.Spec.Trillian.Port) - default: - trillUrl = fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port) - } - if err != nil { - return i.Failed(err) + cm := &v1.ConfigMap{} + err = i.Client.Get(ctx, types.NamespacedName{Name: "ctlog-tree-id-config", Namespace: instance.Namespace}, cm) + if err != nil || cm.Data == nil { + i.Logger.Info("ConfigMap not ready or data is empty, requeuing reconciliation") + return i.Requeue() } - i.Logger.V(1).Info("trillian logserver", "address", trillUrl) - tree, err = i.createTree(ctx, "ctlog-tree", trillUrl, constants.CreateTreeDeadline) + treeId, exists := cm.Data["tree_id"] + if !exists { + err = fmt.Errorf("ConfigMap missing tree_id") + i.Logger.V(1).Error(err, err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: ServerCondition, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) + } + treeIdInt, err := strconv.ParseInt(treeId, 10, 64) if err != nil { + i.Logger.V(1).Error(err, err.Error()) meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: ServerCondition, Status: metav1.ConditionFalse, @@ -93,6 +99,7 @@ func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.C }) return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) } + tree = &trillian.Tree{TreeId: treeIdInt} i.Recorder.Eventf(instance, v1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", tree.TreeId) instance.Status.TreeID = &tree.TreeId diff --git a/internal/controller/ctlog/actions/resolve_tree_test.go b/internal/controller/ctlog/actions/resolve_tree_test.go index 9b0bd684b..21ce7f432 100644 --- a/internal/controller/ctlog/actions/resolve_tree_test.go +++ b/internal/controller/ctlog/actions/resolve_tree_test.go @@ -2,20 +2,16 @@ package actions import ( "context" - "errors" - "fmt" "reflect" "testing" + v1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" - "github.com/google/trillian" . "github.com/onsi/gomega" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/ctlog/utils" - "github.com/securesign/operator/internal/controller/trillian/actions" testAction "github.com/securesign/operator/internal/testing/action" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -115,7 +111,7 @@ func TestResolveTree_Handle(t *testing.T) { type env struct { spec rhtasv1alpha1.CTlogSpec statusTreeId *int64 - createTree createTree + configMap *v1.ConfigMap } type want struct { result *action.Result @@ -133,7 +129,15 @@ func TestResolveTree_Handle(t *testing.T) { TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ctlog-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{ + "tree_id": "5555555", + }, + }, }, want: want{ result: testAction.StatusUpdate(), @@ -146,7 +150,7 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "update tree", + name: "update tree from spec", env: env{ spec: rhtasv1alpha1.CTlogSpec{ TreeID: ptr.To(int64(123456)), @@ -159,7 +163,7 @@ func TestResolveTree_Handle(t *testing.T) { verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { g.Expect(ctlog.Spec.TreeID).ShouldNot(BeNil()) g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) + g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically(">", 0))) g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically("==", *ctlog.Status.TreeID))) }, }, @@ -177,23 +181,29 @@ func TestResolveTree_Handle(t *testing.T) { verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { g.Expect(ctlog.Spec.TreeID).ShouldNot(BeNil()) g.Expect(ctlog.Status.TreeID).ShouldNot(BeNil()) - g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically(">", 0))) + g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically(">", 0))) g.Expect(ctlog.Spec.TreeID).To(HaveValue(BeNumerically("==", *ctlog.Status.TreeID))) g.Expect(ctlog.Status.TreeID).To(HaveValue(BeNumerically("==", 123456))) }, }, }, { - name: "unable to create a new tree", + name: "ConfigMap data is empty", env: env{ spec: rhtasv1alpha1.CTlogSpec{ TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(nil, errors.New("timeout error"), nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ctlog-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{}, + }, }, want: want{ - result: testAction.FailedWithStatusUpdate(fmt.Errorf("could not create trillian tree: timeout error")), + result: testAction.Requeue(), verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { g.Expect(ctlog.Spec.TreeID).Should(BeNil()) g.Expect(ctlog.Status.TreeID).Should(BeNil()) @@ -201,43 +211,19 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "resolve trillian address", + name: "ConfigMap not found", env: env{ spec: rhtasv1alpha1.CTlogSpec{ Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("%s.%s.svc:%d", actions.LogserverDeploymentName, "default", 8091))) - }), }, want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "custom trillian address", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(1234)), Address: "custom-address.namespace.svc"}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("custom-address.namespace.svc:%d", 1234))) - }), - }, - want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "trillian port not specified", - env: env{ - spec: rhtasv1alpha1.CTlogSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: nil}, + result: testAction.Requeue(), + verify: func(g Gomega, ctlog *rhtasv1alpha1.CTlog) { + g.Expect(ctlog.Spec.TreeID).Should(BeNil()) + g.Expect(ctlog.Status.TreeID).Should(BeNil()) }, }, - want: want{ - result: testAction.Failed(fmt.Errorf("resolve treeID: %v", utils.TrillianPortNotSpecified)), - }, }, } for _, tt := range tests { @@ -265,16 +251,17 @@ func TestResolveTree_Handle(t *testing.T) { WithStatusSubresource(instance). Build() - a := testAction.PrepareAction(c, NewResolveTreeAction(func(t *resolveTreeAction) { - if tt.env.createTree == nil { - t.createTree = mockCreateTree(nil, errors.New("createTree should not be executed"), nil) - } else { - t.createTree = tt.env.createTree + if tt.env.configMap != nil { + err := c.Create(ctx, tt.env.configMap) + if err != nil { + t.Fatalf("failed to create config map: %v", err) } - })) + } + + a := testAction.PrepareAction(c, NewResolveTreeAction()) if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { - t.Errorf("CanHandle() = %v, want %v", got, tt.want.result) + t.Errorf("Handle() = %v, want %v", got, tt.want.result) } if tt.want.verify != nil { tt.want.verify(g, instance) @@ -282,12 +269,3 @@ func TestResolveTree_Handle(t *testing.T) { }) } } - -func mockCreateTree(tree *trillian.Tree, err error, verify func(displayName string, trillianURL string, deadline int64)) createTree { - return func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) { - if verify != nil { - verify(displayName, trillianURL, deadline) - } - return tree, err - } -} diff --git a/internal/controller/ctlog/actions/service.go b/internal/controller/ctlog/actions/service.go index b1f35e895..387183af6 100644 --- a/internal/controller/ctlog/actions/service.go +++ b/internal/controller/ctlog/actions/service.go @@ -49,6 +49,7 @@ func (i serviceAction) Handle(ctx context.Context, instance *rhtasv1alpha1.CTlog TargetPort: intstr.FromInt32(MetricsPort), }) } + if err = controllerutil.SetControllerReference(instance, svc, i.Client.Scheme()); err != nil { return i.Failed(fmt.Errorf("could not set controller reference for Service: %w", err)) } diff --git a/internal/controller/ctlog/ctlog_controller.go b/internal/controller/ctlog/ctlog_controller.go index f4a7052fc..ca1c6e3dc 100644 --- a/internal/controller/ctlog/ctlog_controller.go +++ b/internal/controller/ctlog/ctlog_controller.go @@ -22,9 +22,11 @@ import ( olpredicate "github.com/operator-framework/operator-lib/predicate" "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/action/transitions" + "github.com/securesign/operator/internal/controller/constants" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/securesign/operator/internal/controller/ctlog/actions" + "github.com/securesign/operator/internal/controller/ctlog/utils" fulcioActions "github.com/securesign/operator/internal/controller/fulcio/actions" v12 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + actions2 "github.com/securesign/operator/internal/controller/trillian/actions" ) // CTlogReconciler reconciles a CTlog object @@ -93,12 +96,34 @@ func (r *CTlogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl }), transitions.NewToCreatePhaseAction[*rhtasv1alpha1.CTlog](), + actions.NewRBACAction(), actions.NewHandleFulcioCertAction(), actions.NewHandleKeysAction(), + transitions.NewCreateTreeJobAction[*rhtasv1alpha1.CTlog](func(instance *rhtasv1alpha1.CTlog) ( + trillianAddress, instanceName, treeJobConfigMapName, treeJobName, treeDisplayName, trillianDeploymentName string, + namespace string, trillianPort *int32, caPath string, rbac string, labels map[string]string, annotations map[string]string, + treeID *int64, err error, + ) { + caPath, err = utils.CAPath(ctx, r.Client, instance) + labels = constants.LabelsFor(actions.ComponentName, actions.ComponentName, instance.Name) + return instance.Spec.Trillian.Address, + instance.Name, + actions.CtlogTreeJobConfigMapName, + actions.CtlogTreeJobName, + actions.CtlogTreeName, + actions2.LogserverDeploymentName, + instance.Namespace, + instance.Spec.Trillian.Port, + caPath, + actions.RBACName, + labels, + instance.Annotations, + instance.Status.TreeID, + err + }), actions.NewResolveTreeAction(), actions.NewServerConfigAction(), - actions.NewRBACAction(), actions.NewDeployAction(), actions.NewServiceAction(), actions.NewCreateMonitorAction(), diff --git a/internal/controller/ctlog/ctlog_controller_test.go b/internal/controller/ctlog/ctlog_controller_test.go index 54d60daa7..fe81f0585 100644 --- a/internal/controller/ctlog/ctlog_controller_test.go +++ b/internal/controller/ctlog/ctlog_controller_test.go @@ -32,6 +32,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + rutils "github.com/securesign/operator/internal/controller/rekor/utils" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -82,6 +83,12 @@ var _ = Describe("CTlog controller", func() { }) It("should successfully reconcile a custom resource for CTlog", func() { + + By("mocking UseTrillianTLS") + rutils.MockUseTrillianTLS = func(ctx context.Context, serviceAddr string, tlsCACertFile string) (bool, error) { + return false, nil + } + By("creating the custom resource for the Kind CTlog") err := k8sClient.Get(ctx, typeNamespaceName, instance) if err != nil && errors.IsNotFound(err) { diff --git a/internal/controller/ctlog/utils/ctlog_deployment.go b/internal/controller/ctlog/utils/ctlog_deployment.go index e8af3ba9e..6df9441da 100644 --- a/internal/controller/ctlog/utils/ctlog_deployment.go +++ b/internal/controller/ctlog/utils/ctlog_deployment.go @@ -1,19 +1,23 @@ package utils import ( + "context" + "errors" "fmt" "strconv" "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/utils" "github.com/securesign/operator/internal/controller/constants" + rutils "github.com/securesign/operator/internal/controller/rekor/utils" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" ) -func CreateDeployment(instance *v1alpha1.CTlog, deploymentName string, sa string, labels map[string]string, serverPort, metricsPort int32) (*appsv1.Deployment, error) { +func CreateDeployment(ctx context.Context, client client.Client, instance *v1alpha1.CTlog, deploymentName string, sa string, labels map[string]string, serverPort, metricsPort int32) (*appsv1.Deployment, error) { switch { case instance.Status.ServerConfigRef == nil: return nil, fmt.Errorf("CreateCTLogDeployment: %w", ServerConfigNotSpecified) @@ -120,6 +124,22 @@ func CreateDeployment(instance *v1alpha1.CTlog, deploymentName string, sa string }, }, } + + // TLS communication to Trillian logserver + trillianSvc := fmt.Sprintf(instance.Spec.Trillian.Address+":%d", *instance.Spec.Trillian.Port) + caPath, err := CAPath(ctx, client, instance) + if err != nil { + return nil, errors.New("failed to get CA path: " + err.Error()) + } + + useTLS := false + if useTLS, err = rutils.UseTrillianTLS(ctx, trillianSvc, caPath); err != nil { + return nil, errors.New("failed to check TLS: " + err.Error()) + } + if useTLS { + dep.Spec.Template.Spec.Containers[0].Args = append(dep.Spec.Template.Spec.Containers[0].Args, "--trillian_tls_ca_cert_file", caPath) + } + utils.SetProxyEnvs(dep) return dep, nil } diff --git a/internal/controller/ctlog/utils/tls.go b/internal/controller/ctlog/utils/tls.go new file mode 100644 index 000000000..bfeb1ae1f --- /dev/null +++ b/internal/controller/ctlog/utils/tls.go @@ -0,0 +1,45 @@ +package utils + +import ( + "context" + "fmt" + + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func UseTLS(instance *rhtasv1alpha1.CTlog) bool { + + if instance == nil { + return false + } + // TLS enabled on Trillian logserver + if instance.Spec.TrustedCA != nil || kubernetes.IsOpenShift() { + return true + } + + return false +} + +func CAPath(ctx context.Context, cli client.Client, instance *rhtasv1alpha1.CTlog) (string, error) { + if instance.Spec.TrustedCA != nil { + cfgTrust, err := kubernetes.GetConfigMap(ctx, cli, instance.Namespace, instance.Spec.TrustedCA.Name) + if err != nil { + return "", err + } + if len(cfgTrust.Data) != 1 { + err = fmt.Errorf("%s ConfigMap can contain only 1 record", instance.Spec.TrustedCA.Name) + return "", err + } + for key := range cfgTrust.Data { + return "/var/run/configs/tas/ca-trust/" + key, nil + } + } + + if instance.Spec.TrustedCA == nil && kubernetes.IsOpenShift() { + return "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt", nil + } + + return "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", nil +} diff --git a/internal/controller/rekor/actions/constants.go b/internal/controller/rekor/actions/constants.go index 359729279..77a38c1c6 100644 --- a/internal/controller/rekor/actions/constants.go +++ b/internal/controller/rekor/actions/constants.go @@ -23,4 +23,8 @@ const ( ServerCondition = "ServerAvailable" RedisCondition = "RedisAvailable" SignerCondition = "SignerAvailable" + RekorTreeName = "rekor-tree" + RekorTreeJobName = "rekor-create-tree" + RekorTreeJobCondition = "RekorTreeJobAvailable" + RekorTreeJobConfigMapName = "rekor-tree-id-config" ) diff --git a/internal/controller/rekor/actions/rbac.go b/internal/controller/rekor/actions/rbac.go index 22144f21e..daba4e9dd 100644 --- a/internal/controller/rekor/actions/rbac.go +++ b/internal/controller/rekor/actions/rbac.go @@ -64,7 +64,7 @@ func (i rbacAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) * { APIGroups: []string{""}, Resources: []string{"configmaps"}, - Verbs: []string{"create", "get", "update"}, + Verbs: []string{"create", "get", "update", "patch"}, }, { APIGroups: []string{""}, diff --git a/internal/controller/rekor/actions/server/deployment.go b/internal/controller/rekor/actions/server/deployment.go index 974a7e478..bb9531a9b 100644 --- a/internal/controller/rekor/actions/server/deployment.go +++ b/internal/controller/rekor/actions/server/deployment.go @@ -48,9 +48,14 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Rekor) insCopy.Spec.Trillian.Address = fmt.Sprintf("%s.%s.svc", actions2.LogserverDeploymentName, instance.Namespace) } i.Logger.V(1).Info("trillian logserver", "address", insCopy.Spec.Trillian.Address) - dp, err := utils.CreateRekorDeployment(insCopy, actions.ServerDeploymentName, actions.RBACName, labels) + dp, err := utils.CreateRekorDeployment(ctx, i.Client, insCopy, actions.ServerDeploymentName, actions.RBACName, labels) if err == nil { - err = cutils.SetTrustedCA(&dp.Spec.Template, cutils.TrustedCAAnnotationToReference(instance.Annotations)) + caTrustRef := cutils.TrustedCAAnnotationToReference(instance.Annotations) + // override if spec.trustedCA is defined + if instance.Spec.TrustedCA != nil { + caTrustRef = instance.Spec.TrustedCA + } + err = cutils.SetTrustedCA(&dp.Spec.Template, caTrustRef) } if err != nil { diff --git a/internal/controller/rekor/actions/server/resolve_tree.go b/internal/controller/rekor/actions/server/resolve_tree.go index 122511b8c..5f1dfe402 100644 --- a/internal/controller/rekor/actions/server/resolve_tree.go +++ b/internal/controller/rekor/actions/server/resolve_tree.go @@ -3,27 +3,22 @@ package server import ( "context" "fmt" + "strconv" "github.com/google/trillian" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" - "github.com/securesign/operator/internal/controller/common" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/constants" "github.com/securesign/operator/internal/controller/rekor/actions" - "github.com/securesign/operator/internal/controller/rekor/utils" - actions2 "github.com/securesign/operator/internal/controller/trillian/actions" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) -type createTree func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) - func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtasv1alpha1.Rekor] { - a := &resolveTreeAction{ - createTree: common.CreateTrillianTree, - } + a := &resolveTreeAction{} for _, opt := range opts { opt(a) @@ -33,7 +28,6 @@ func NewResolveTreeAction(opts ...func(*resolveTreeAction)) action.Action[*rhtas type resolveTreeAction struct { action.BaseAction - createTree createTree } func (i resolveTreeAction) Name() string { @@ -63,23 +57,35 @@ func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.R } var err error var tree *trillian.Tree - var trillUrl string - switch { - case instance.Spec.Trillian.Port == nil: - err = fmt.Errorf("%s: %v", i.Name(), utils.TrillianPortNotSpecified) - case instance.Spec.Trillian.Address == "": - trillUrl = fmt.Sprintf("%s.%s.svc:%d", actions2.LogserverDeploymentName, instance.Namespace, *instance.Spec.Trillian.Port) - default: - trillUrl = fmt.Sprintf("%s:%d", instance.Spec.Trillian.Address, *instance.Spec.Trillian.Port) - } - if err != nil { - return i.Failed(err) + cm := &v1.ConfigMap{} + err = i.Client.Get(ctx, types.NamespacedName{Name: "rekor-tree-id-config", Namespace: instance.Namespace}, cm) + if err != nil || cm.Data == nil { + i.Logger.Info("ConfigMap not ready or data is empty, requeuing reconciliation") + return i.Requeue() } - i.Logger.V(1).Info("trillian logserver", "address", trillUrl) - tree, err = i.createTree(ctx, "rekor-tree", trillUrl, constants.CreateTreeDeadline) + treeId, exists := cm.Data["tree_id"] + if !exists { + err = fmt.Errorf("ConfigMap missing tree_id") + i.Logger.V(1).Error(err, err.Error()) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: actions.ServerCondition, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: constants.Ready, + Status: metav1.ConditionFalse, + Reason: constants.Failure, + Message: err.Error(), + }) + return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) + } + treeIdInt, err := strconv.ParseInt(treeId, 10, 64) if err != nil { + i.Logger.V(1).Error(err, err.Error()) meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ Type: actions.ServerCondition, Status: metav1.ConditionFalse, @@ -94,6 +100,7 @@ func (i resolveTreeAction) Handle(ctx context.Context, instance *rhtasv1alpha1.R }) return i.FailedWithStatusUpdate(ctx, fmt.Errorf("could not create trillian tree: %v", err), instance) } + tree = &trillian.Tree{TreeId: treeIdInt} i.Recorder.Eventf(instance, v1.EventTypeNormal, "TrillianTreeCreated", "New Trillian tree created: %d", tree.TreeId) instance.Status.TreeID = &tree.TreeId diff --git a/internal/controller/rekor/actions/server/resolve_tree_test.go b/internal/controller/rekor/actions/server/resolve_tree_test.go index 26917022e..296fbc5a2 100644 --- a/internal/controller/rekor/actions/server/resolve_tree_test.go +++ b/internal/controller/rekor/actions/server/resolve_tree_test.go @@ -2,19 +2,15 @@ package server import ( "context" - "errors" - "fmt" "reflect" "testing" - "github.com/google/trillian" . "github.com/onsi/gomega" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/action" "github.com/securesign/operator/internal/controller/constants" - "github.com/securesign/operator/internal/controller/rekor/utils" - "github.com/securesign/operator/internal/controller/trillian/actions" testAction "github.com/securesign/operator/internal/testing/action" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -114,7 +110,7 @@ func TestResolveTree_Handle(t *testing.T) { type env struct { spec rhtasv1alpha1.RekorSpec statusTreeId *int64 - createTree createTree + configMap *v1.ConfigMap } type want struct { result *action.Result @@ -132,7 +128,15 @@ func TestResolveTree_Handle(t *testing.T) { TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rekor-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{ + "tree_id": "5555555", + }, + }, }, want: want{ result: testAction.StatusUpdate(), @@ -145,7 +149,7 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "update tree", + name: "update tree from spec", env: env{ spec: rhtasv1alpha1.RekorSpec{ TreeID: ptr.To(int64(123456)), @@ -183,16 +187,22 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "unable to create a new tree", + name: "ConfigMap data is empty", env: env{ spec: rhtasv1alpha1.RekorSpec{ TreeID: nil, Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(nil, errors.New("timeout error"), nil), + configMap: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rekor-tree-id-config", + Namespace: "default", + }, + Data: map[string]string{}, + }, }, want: want{ - result: testAction.FailedWithStatusUpdate(fmt.Errorf("could not create trillian tree: timeout error")), + result: testAction.Requeue(), verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { g.Expect(rekor.Spec.TreeID).Should(BeNil()) g.Expect(rekor.Status.TreeID).Should(BeNil()) @@ -200,43 +210,19 @@ func TestResolveTree_Handle(t *testing.T) { }, }, { - name: "resolve trillian address", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(1234))}, - }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("%s.%s.svc:%d", actions.LogserverDeploymentName, "default", 1234))) - }), - }, - want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "custom trillian address", + name: "ConfigMap not found", env: env{ spec: rhtasv1alpha1.RekorSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(1234)), Address: "custom-address.namespace.svc"}, + Trillian: rhtasv1alpha1.TrillianService{Port: ptr.To(int32(8091))}, }, - createTree: mockCreateTree(&trillian.Tree{TreeId: 5555555}, nil, func(displayName string, trillianURL string, deadline int64) { - g.Expect(trillianURL).Should(Equal(fmt.Sprintf("custom-address.namespace.svc:%d", 1234))) - }), }, want: want{ - result: testAction.StatusUpdate(), - }, - }, - { - name: "trillian port not specified", - env: env{ - spec: rhtasv1alpha1.RekorSpec{ - Trillian: rhtasv1alpha1.TrillianService{Port: nil}, + result: testAction.Requeue(), + verify: func(g Gomega, rekor *rhtasv1alpha1.Rekor) { + g.Expect(rekor.Spec.TreeID).Should(BeNil()) + g.Expect(rekor.Status.TreeID).Should(BeNil()) }, }, - want: want{ - result: testAction.Failed(fmt.Errorf("resolve treeID: %v", utils.TrillianPortNotSpecified)), - }, }, } for _, tt := range tests { @@ -264,13 +250,14 @@ func TestResolveTree_Handle(t *testing.T) { WithStatusSubresource(instance). Build() - a := testAction.PrepareAction(c, NewResolveTreeAction(func(t *resolveTreeAction) { - if tt.env.createTree == nil { - t.createTree = mockCreateTree(nil, errors.New("createTree should not be executed"), nil) - } else { - t.createTree = tt.env.createTree + if tt.env.configMap != nil { + err := c.Create(ctx, tt.env.configMap) + if err != nil { + t.Fatalf("failed to create config map: %v", err) } - })) + } + + a := testAction.PrepareAction(c, NewResolveTreeAction()) if got := a.Handle(ctx, instance); !reflect.DeepEqual(got, tt.want.result) { t.Errorf("CanHandle() = %v, want %v", got, tt.want.result) @@ -281,12 +268,3 @@ func TestResolveTree_Handle(t *testing.T) { }) } } - -func mockCreateTree(tree *trillian.Tree, err error, verify func(displayName string, trillianURL string, deadline int64)) createTree { - return func(ctx context.Context, displayName string, trillianURL string, deadline int64) (*trillian.Tree, error) { - if verify != nil { - verify(displayName, trillianURL, deadline) - } - return tree, err - } -} diff --git a/internal/controller/rekor/rekor_controller.go b/internal/controller/rekor/rekor_controller.go index 6cfeb776d..380afadb1 100644 --- a/internal/controller/rekor/rekor_controller.go +++ b/internal/controller/rekor/rekor_controller.go @@ -24,12 +24,14 @@ import ( olpredicate "github.com/operator-framework/operator-lib/predicate" "github.com/securesign/operator/internal/controller/annotations" "github.com/securesign/operator/internal/controller/common/action/transitions" + "github.com/securesign/operator/internal/controller/constants" actions2 "github.com/securesign/operator/internal/controller/rekor/actions" backfillredis "github.com/securesign/operator/internal/controller/rekor/actions/backfillRedis" "github.com/securesign/operator/internal/controller/rekor/actions/redis" "github.com/securesign/operator/internal/controller/rekor/actions/server" "github.com/securesign/operator/internal/controller/rekor/actions/ui" + "github.com/securesign/operator/internal/controller/rekor/utils" v13 "k8s.io/api/core/v1" v1 "k8s.io/api/networking/v1" "k8s.io/client-go/tools/record" @@ -43,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + actions3 "github.com/securesign/operator/internal/controller/trillian/actions" batchv1 "k8s.io/api/batch/v1" ) @@ -109,6 +112,28 @@ func (r *RekorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl actions2.NewRBACAction(), server.NewShardingConfigAction(), + transitions.NewCreateTreeJobAction[*rhtasv1alpha1.Rekor](func(instance *rhtasv1alpha1.Rekor) ( + trillianAddress, instanceName, treeJobConfigMapName, treeJobName, treeDisplayName, trillianDeploymentName string, + namespace string, trillianPort *int32, caPath string, rbac string, labels map[string]string, annotations map[string]string, + treeID *int64, err error, + ) { + caPath, err = utils.CAPath(ctx, r.Client, instance) + labels = constants.LabelsFor(actions2.ServerComponentName, actions2.ServerDeploymentName, instance.Name) + return instance.Spec.Trillian.Address, + instance.Name, + actions2.RekorTreeJobConfigMapName, + actions2.RekorTreeJobName, + actions2.RekorTreeName, + actions3.LogserverDeploymentName, + instance.Namespace, + instance.Spec.Trillian.Port, + caPath, + actions2.RBACName, + labels, + instance.Annotations, + instance.Status.TreeID, + err + }), server.NewResolveTreeAction(), server.NewCreatePvcAction(), server.NewDeployAction(), diff --git a/internal/controller/rekor/rekor_controller_test.go b/internal/controller/rekor/rekor_controller_test.go index 3b2a074ee..5961dc93b 100644 --- a/internal/controller/rekor/rekor_controller_test.go +++ b/internal/controller/rekor/rekor_controller_test.go @@ -41,6 +41,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + utils2 "github.com/securesign/operator/internal/controller/rekor/utils" batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -96,6 +97,12 @@ var _ = Describe("Rekor controller", func() { }) It("should successfully reconcile a custom resource for Rekor", func() { + + By("mocking UseTrillianTLS") + utils2.MockUseTrillianTLS = func(ctx context.Context, serviceAddr string, tlsCACertFile string) (bool, error) { + return false, nil + } + By("creating the custom resource for the Kind Rekor") err := k8sClient.Get(ctx, typeNamespaceName, instance) if err != nil && errors.IsNotFound(err) { diff --git a/internal/controller/rekor/utils/rekor_deployment.go b/internal/controller/rekor/utils/rekor_deployment.go index e4b60ca49..1f260642a 100644 --- a/internal/controller/rekor/utils/rekor_deployment.go +++ b/internal/controller/rekor/utils/rekor_deployment.go @@ -1,9 +1,12 @@ package utils import ( + "context" + "errors" "fmt" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/securesign/operator/api/v1alpha1" "github.com/securesign/operator/internal/controller/common/utils" @@ -13,7 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func CreateRekorDeployment(instance *v1alpha1.Rekor, dpName string, sa string, labels map[string]string) (*apps.Deployment, error) { +func CreateRekorDeployment(ctx context.Context, client client.Client, instance *v1alpha1.Rekor, dpName string, sa string, labels map[string]string) (*apps.Deployment, error) { switch { case instance.Status.ServerConfigRef == nil: return nil, fmt.Errorf("CreateRekorDeployment: %w", ServerConfigNotSpecified) @@ -200,6 +203,21 @@ func CreateRekorDeployment(instance *v1alpha1.Rekor, dpName string, sa string, l }, }, } + + // TLS communication to Trillian logserver + trillianSvc := fmt.Sprintf(instance.Spec.Trillian.Address+":%d", *instance.Spec.Trillian.Port) + caPath, err := CAPath(ctx, client, instance) + if err != nil { + return nil, errors.New("failed to get CA path: " + err.Error()) + } + useTLS := false + if useTLS, err = UseTrillianTLS(ctx, trillianSvc, caPath); err != nil { + return nil, errors.New("failed to check TLS: " + err.Error()) + } + if useTLS { + dep.Spec.Template.Spec.Containers[0].Args = append(dep.Spec.Template.Spec.Containers[0].Args, "--trillian_log_server.tls=true") + } + utils.SetProxyEnvs(dep) return dep, nil } diff --git a/internal/controller/rekor/utils/tls.go b/internal/controller/rekor/utils/tls.go new file mode 100644 index 000000000..b26741261 --- /dev/null +++ b/internal/controller/rekor/utils/tls.go @@ -0,0 +1,92 @@ +package utils + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Mock used in tests +var MockUseTrillianTLS func(ctx context.Context, serviceAddr string, tlsCACertFile string) (bool, error) + +// checks if trillian-logserver service supports TLS +func UseTrillianTLS(ctx context.Context, serviceAddr string, tlsCACertFile string) (bool, error) { + + if MockUseTrillianTLS != nil { + return MockUseTrillianTLS(ctx, serviceAddr, "") + } + + if kubernetes.IsOpenShift() { + return true, nil + } + + timeout := 5 * time.Second + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + hostname := serviceAddr + if idx := strings.Index(serviceAddr, ":"); idx != -1 { + hostname = serviceAddr[:idx] + } + + var creds credentials.TransportCredentials + if tlsCACertFile != "" { + tlsCaCert, err := os.ReadFile(filepath.Clean(tlsCACertFile)) + if err != nil { + return false, fmt.Errorf("failed to load tls ca cert: %v", err) + } + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(tlsCaCert) { + return false, fmt.Errorf("failed to append CA certificate to pool") + } + creds = credentials.NewTLS(&tls.Config{ + ServerName: hostname, + RootCAs: certPool, + MinVersion: tls.VersionTLS12, + }) + } + + conn, err := grpc.DialContext(ctx, serviceAddr, grpc.WithTransportCredentials(creds), grpc.WithBlock()) + if err != nil { + fmt.Printf("gRPC service at %s is not TLS secured: %v\n", serviceAddr, err) + return false, nil + } + if err := conn.Close(); err != nil { + return false, fmt.Errorf("failed to close connection: %v", err) + } + + return true, nil +} + +func CAPath(ctx context.Context, cli client.Client, instance *rhtasv1alpha1.Rekor) (string, error) { + if instance.Spec.TrustedCA != nil { + cfgTrust, err := kubernetes.GetConfigMap(ctx, cli, instance.Namespace, instance.Spec.TrustedCA.Name) + if err != nil { + return "", err + } + if len(cfgTrust.Data) != 1 { + err = fmt.Errorf("%s ConfigMap can contain only 1 record", instance.Spec.TrustedCA.Name) + return "", err + } + for key := range cfgTrust.Data { + return "/var/run/configs/tas/ca-trust/" + key, nil + } + } + + if instance.Spec.TrustedCA == nil && kubernetes.IsOpenShift() { + return "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt", nil + } + + return "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", nil +} diff --git a/internal/controller/trillian/actions/logserver/deployment.go b/internal/controller/trillian/actions/logserver/deployment.go index faef2bd34..7da02f2d3 100644 --- a/internal/controller/trillian/actions/logserver/deployment.go +++ b/internal/controller/trillian/actions/logserver/deployment.go @@ -14,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" rhtasv1alpha1 "github.com/securesign/operator/api/v1alpha1" + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" ) func NewDeployAction() action.Action[*rhtasv1alpha1.Trillian] { @@ -39,6 +40,25 @@ func (i deployAction) Handle(ctx context.Context, instance *rhtasv1alpha1.Trilli updated bool ) + // TLS + switch { + case instance.Spec.Server.TLS.CertRef != nil: + instance.Status.Server.TLS = instance.Spec.Server.TLS + case kubernetes.IsOpenShift(): + instance.Status.Server.TLS = rhtasv1alpha1.TLS{ + CertRef: &rhtasv1alpha1.SecretKeySelector{ + LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: instance.Name + "-trillian-server-tls"}, + Key: "tls.crt", + }, + PrivateKeyRef: &rhtasv1alpha1.SecretKeySelector{ + LocalObjectReference: rhtasv1alpha1.LocalObjectReference{Name: instance.Name + "-trillian-server-tls"}, + Key: "tls.key", + }, + } + default: + i.Logger.V(1).Info("Communication to trillian log server is insecure") + } + labels := constants.LabelsFor(actions.LogServerComponentName, actions.LogserverDeploymentName, instance.Name) server, err := trillianUtils.CreateLogServerDeployment(ctx, i.Client, instance, constants.TrillianServerImage, actions.LogserverDeploymentName, actions.RBACName, labels) if err != nil { diff --git a/internal/controller/trillian/actions/logserver/service.go b/internal/controller/trillian/actions/logserver/service.go index 774911f48..f3afd8b4c 100644 --- a/internal/controller/trillian/actions/logserver/service.go +++ b/internal/controller/trillian/actions/logserver/service.go @@ -53,6 +53,14 @@ func (i createServiceAction) Handle(ctx context.Context, instance *rhtasv1alpha1 }) } + //TLS: Annotate service + if k8sutils.IsOpenShift() && instance.Spec.Server.TLS.CertRef == nil { + if logserverService.Annotations == nil { + logserverService.Annotations = make(map[string]string) + } + logserverService.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = instance.Name + "-trillian-server-tls" + } + if err = controllerutil.SetControllerReference(instance, logserverService, i.Client.Scheme()); err != nil { return i.Failed(fmt.Errorf("could not set controller reference for logserver Service: %w", err)) } diff --git a/internal/controller/trillian/trillian_controller.go b/internal/controller/trillian/trillian_controller.go index 6d75ee00d..5d2afc67a 100644 --- a/internal/controller/trillian/trillian_controller.go +++ b/internal/controller/trillian/trillian_controller.go @@ -28,6 +28,7 @@ import ( "github.com/securesign/operator/internal/controller/common/action/transitions" "github.com/securesign/operator/internal/controller/common/action" + actions2 "github.com/securesign/operator/internal/controller/ctlog/actions" "github.com/securesign/operator/internal/controller/trillian/actions" "github.com/securesign/operator/internal/controller/trillian/actions/db" "github.com/securesign/operator/internal/controller/trillian/actions/logserver" @@ -89,7 +90,7 @@ func (r *TrillianReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c target := instance.DeepCopy() actions := []action.Action[*rhtasv1alpha1.Trillian]{ transitions.NewToPendingPhaseAction[*rhtasv1alpha1.Trillian](func(t *rhtasv1alpha1.Trillian) []string { - components := []string{actions.ServerCondition, actions.SignerCondition} + components := []string{actions.ServerCondition, actions.SignerCondition, actions2.CtlogTreeJobCondition} if utils.IsEnabled(t.Spec.Db.Create) { components = append(components, actions.DbCondition) } diff --git a/internal/controller/trillian/utils/server-deployment.go b/internal/controller/trillian/utils/server-deployment.go index 8416808d0..00370528e 100644 --- a/internal/controller/trillian/utils/server-deployment.go +++ b/internal/controller/trillian/utils/server-deployment.go @@ -180,6 +180,15 @@ func CreateLogServerDeployment(ctx context.Context, client client.Client, instan dep.Spec.Template.Spec.Containers[0].Args = append(dep.Spec.Template.Spec.Containers[0].Args, "--mysql_server_name", mysqlServerName) } + // Server TLS communication + if err := utils.SetTLS(&dep.Spec.Template, instance.Status.Server.TLS); err != nil { + return nil, errors.New("could not set TLS: " + err.Error()) + } + if instance.Status.Server.TLS.CertRef != nil { + dep.Spec.Template.Spec.Containers[0].Args = append(dep.Spec.Template.Spec.Containers[0].Args, "--tls_cert_file", "/var/run/secrets/tas/tls.crt") + dep.Spec.Template.Spec.Containers[0].Args = append(dep.Spec.Template.Spec.Containers[0].Args, "--tls_key_file", "/var/run/secrets/tas/tls.key") + } + utils.SetProxyEnvs(dep) return dep, nil }