diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/pkg/util/kube/pki.go b/pkg/util/kube/pki.go new file mode 100644 index 0000000..6f8055c --- /dev/null +++ b/pkg/util/kube/pki.go @@ -0,0 +1,43 @@ +package kube + +import ( + "context" + "crypto" + + corev1 "k8s.io/api/core/v1" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corelisters "k8s.io/client-go/listers/core/v1" +) + +func SecretTLSKey(ctx context.Context, secretLister corelisters.SecretLister, namespace, name string) (crypto.Signer, error) { + return SecretTLSKeyRef(ctx, secretLister, namespace, name, corev1.TLSPrivateKeyKey) +} + +//SecretTLSKeyRef will fetch the key from the secret. +func SecretTLSKeyRef(ctx context.Context, secretLister corelisters.SecretLister, namespace, name, keyName string) (crypto.Signer, error) { + secret, err := secretLister.Secrets(namespace).Get(name) + if err != nil { + return nil, err + } + + key, _, err := ParseTLSKeyFromSecret(secret, keyName) + if err != nil { + return nil, err + } + return key, nil +} + +func ParseTLSKeyFromSecret(secret *corev1.Secret, keyName string) (crypto.Signer, []byte, error) { + keyBytes, ok := secret.Data[keyName] + if !ok { + return nil, nil, errors.NewInvalidData("no data for %q in secret '%s/%s'", keyName, secret.Namespace, secret.Name) + } + + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return nil, keyBytes, errors.NewInvalidData(err.Error()) + } + return key, keyBytes, nil +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/pkg/util/kube/pki.go b/pkg/util/kube/pki.go new file mode 100644 index 0000000..6f8055c --- /dev/null +++ b/pkg/util/kube/pki.go @@ -0,0 +1,43 @@ +package kube + +import ( + "context" + "crypto" + + corev1 "k8s.io/api/core/v1" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corelisters "k8s.io/client-go/listers/core/v1" +) + +func SecretTLSKey(ctx context.Context, secretLister corelisters.SecretLister, namespace, name string) (crypto.Signer, error) { + return SecretTLSKeyRef(ctx, secretLister, namespace, name, corev1.TLSPrivateKeyKey) +} + +//SecretTLSKeyRef will fetch the key from the secret. +func SecretTLSKeyRef(ctx context.Context, secretLister corelisters.SecretLister, namespace, name, keyName string) (crypto.Signer, error) { + secret, err := secretLister.Secrets(namespace).Get(name) + if err != nil { + return nil, err + } + + key, _, err := ParseTLSKeyFromSecret(secret, keyName) + if err != nil { + return nil, err + } + return key, nil +} + +func ParseTLSKeyFromSecret(secret *corev1.Secret, keyName string) (crypto.Signer, []byte, error) { + keyBytes, ok := secret.Data[keyName] + if !ok { + return nil, nil, errors.NewInvalidData("no data for %q in secret '%s/%s'", keyName, secret.Namespace, secret.Name) + } + + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return nil, keyBytes, errors.NewInvalidData(err.Error()) + } + return key, keyBytes, nil +} diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index e29bd42..1920afa 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -1,46 +1,75 @@ package pki import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" + "fmt" + "math/big" "net" "net/url" "strings" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" ) -// URLsFromString parses the urls from the string array -func URLsFromString(urlStrs []string) ([]*url.URL, error) { +func IPAddressesForCertificate(crt *v1.Certificate) []net.IP { + var ipAddresses []net.IP + var ip net.IP + for _, ipName := range crt.Spec.IPAddresses { + ip = net.ParseIP(ipName) + if ip != nil { + ipAddresses = append(ipAddresses, ip) + } + } + return ipAddresses +} + +func URIsForCertificate(crt *v1.Certificate) ([]*url.URL, error) { + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, fmt.Errorf("failed to parse URIs: %s", err) + } + + return uris, nil +} + +func DNSNamesForCertificate(crt *v1.Certificate) ([]string, error) { + _, err := URLsFromStrings(crt.Spec.DNSNames) + if err != nil { + return nil, fmt.Errorf("failed to parse DNSNames: %s", err) + } + + return crt.Spec.DNSNames, nil +} + +func URLsFromStrings(urlStrs []string) ([]*url.URL, error) { var urls []*url.URL var errs []string + for _, urlStr := range urlStrs { url, err := url.Parse(urlStr) if err != nil { errs = append(errs, err.Error()) continue } + urls = append(urls, url) } if len(errs) > 0 { return nil, errors.New(strings.Join(errs, ", ")) } + return urls, nil } -// URLsToString converts the array of *url.URL object to the string array -func URLsToString(urls []*url.URL) []string { - var urlStrs []string - for _, url := range urls { - if urls == nil { - panic("provided url to string is nil") - } - - urlStrs = append(urlStrs, url.String()) - } - - return urlStrs -} - -// IPAddressesToString converts the ip address to the string func IPAddressesToString(ipAddresses []net.IP) []string { var ipNames []string for _, ip := range ipAddresses { @@ -48,3 +77,581 @@ } return ipNames } + +func URLsToString(uris []*url.URL) []string { + var uriStrs []string + for _, uri := range uris { + if uri == nil { + panic("provided uri to string is nil") + } + + uriStrs = append(uriStrs, uri.String()) + } + + return uriStrs +} + +func removeDuplicates(in []string) []string { + var found []string +Outer: + for _, i := range in { + for _, i2 := range found { + if i2 == i { + continue Outer + } + } + found = append(found, i) + } + return found +} + +// OrganizationForCertificate will return the Organization to set for the +// Certificate resource. +// If an Organization is not specifically set, a default will be used. +func OrganizationForCertificate(crt *v1.Certificate) []string { + if crt.Spec.Subject == nil { + return nil + } + return crt.Spec.Subject.Organizations +} + +// SubjectForCertificate will return the Subject from the Certificate resource or an empty one if it is not set +func SubjectForCertificate(crt *v1.Certificate) v1.X509Subject { + if crt.Spec.Subject == nil { + return v1.X509Subject{} + } + + return *crt.Spec.Subject +} + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) + +func BuildKeyUsages(usages []v1.KeyUsage, isCA bool) (ku x509.KeyUsage, eku []x509.ExtKeyUsage, err error) { + var unk []v1.KeyUsage + if isCA { + ku |= x509.KeyUsageCertSign + } + if len(usages) == 0 { + usages = append(usages, v1.DefaultKeyUsages()...) + } + for _, u := range usages { + if kuse, ok := apiutil.KeyUsageType(u); ok { + ku |= kuse + } else if ekuse, ok := apiutil.ExtKeyUsageType(u); ok { + eku = append(eku, ekuse) + } else { + unk = append(unk, u) + } + } + if len(unk) > 0 { + err = fmt.Errorf("unknown key usages: %v", unk) + } + return +} + +func BuildCertManagerKeyUsages(ku x509.KeyUsage, eku []x509.ExtKeyUsage) []v1.KeyUsage { + usages := apiutil.KeyUsageStrings(ku) + usages = append(usages, apiutil.ExtKeyUsageStrings(eku)...) + + return usages +} + +// GenerateCSR will generate a new *x509.CertificateRequest template to be used +// by issuers that utilise CSRs to obtain Certificates. +// The CSR will not be signed, and should be passed to either EncodeCSR or +// to the x509.CreateCertificateRequest function. +func GenerateCSR(crt *v1.Certificate) (*x509.CertificateRequest, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + iPAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + + dnsNames, err := DNSNamesForCertificate(crt) + if err != nil { + return nil, err + } + + uriNames, err := URIsForCertificate(crt) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 && len(crt.Spec.EmailAddresses) == 0 && len(crt.Spec.IPAddresses) == 0 { + return nil, fmt.Errorf("no common name, DNS name, URI SAN, or Email SAN specified on certificate") + } + + pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + // var extraExtensions []pkix.Extension + // if crt.Spec.EncodeUsagesInRequest == nil || *crt.Spec.EncodeUsagesInRequest { + // extraExtensions, err = buildKeyUsagesExtensionsForCertificate(crt) + // if err != nil { + // return nil, err + // } + // } + + // if utilfeature.DefaultFeatureGate.Enabled(feature.UseCertificateRequestBasicConstraints) { + // extension, err := buildBasicConstraintsExtensionsForCertificate(crt.Spec.IsCA) + // if err != nil { + // return nil, err + // } + // extraExtensions = append(extraExtensions, extension) + // } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + RawSubject: rawSubject, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + //ExtraExtensions: extraExtensions, + }, nil + } else { + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + // ExtraExtensions: extraExtensions, + }, nil + } + +} + +// func buildKeyUsagesExtensionsForCertificate(crt *v1.Certificate) ([]pkix.Extension, error) { +// ku, ekus, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) +// if err != nil { +// return nil, fmt.Errorf("failed to build key usages: %w", err) +// } + +// usage, err := buildASN1KeyUsageRequest(ku) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode usages: %w", err) +// } +// asn1ExtendedUsages := []asn1.ObjectIdentifier{} +// for _, eku := range ekus { +// if oid, ok := OIDFromExtKeyUsage(eku); ok { +// asn1ExtendedUsages = append(asn1ExtendedUsages, oid) +// } +// } + +// extraExtensions := []pkix.Extension{usage} +// if len(ekus) > 0 { +// extendedUsage := pkix.Extension{ +// Id: OIDExtensionExtendedKeyUsage, +// } +// extendedUsage.Value, err = asn1.Marshal(asn1ExtendedUsages) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode extended usages: %w", err) +// } + +// extraExtensions = append(extraExtensions, extendedUsage) +// } +// return extraExtensions, nil +// } + +// func buildBasicConstraintsExtensionsForCertificate(isCA bool) (pkix.Extension, error) { + +// basicConstraints := pkix.Extension{ +// Id: OIDExtensionBasicConstraints, +// } + +// constraint := struct { +// IsCA bool +// }{ +// IsCA: isCA, +// } + +// var err error +// basicConstraints.Value, err = asn1.Marshal(constraint) +// if err != nil { +// return pkix.Extension{}, err +// } + +// return basicConstraints, nil +// } + +// GenerateTemplate will create a x509.Certificate for the given Certificate resource. +// This should create a Certificate template that is equivalent to the CertificateRequest +// generated by GenerateCSR. +// The PublicKey field must be populated by the caller. +func GenerateTemplate(crt *v1.Certificate) (*x509.Certificate, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + dnsNames := crt.Spec.DNSNames + ipAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, err + } + keyUsages, extKeyUsages, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(ipAddresses) == 0 && len(uris) == 0 && len(crt.Spec.EmailAddresses) == 0 { + return nil, fmt.Errorf("no common name or subject alt names requested on certificate") + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + certDuration := apiutil.DefaultCertDuration(crt.Spec.Duration) + + pubKeyAlgo, _, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + RawSubject: rawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } else { + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } +} + +// GenerateTemplate will create a x509.Certificate for the given +// CertificateRequest resource +func GenerateTemplateFromCertificateRequest(cr *v1.CertificateRequest) (*x509.Certificate, error) { + certDuration := apiutil.DefaultCertDuration(cr.Spec.Duration) + keyUsage, extKeyUsage, err := BuildKeyUsages(cr.Spec.Usages, cr.Spec.IsCA) + if err != nil { + return nil, err + } + return GenerateTemplateFromCSRPEMWithUsages(cr.Spec.Request, certDuration, cr.Spec.IsCA, keyUsage, extKeyUsage) +} + +func GenerateTemplateFromCSRPEM(csrPEM []byte, duration time.Duration, isCA bool) (*x509.Certificate, error) { + var ( + ku x509.KeyUsage + eku []x509.ExtKeyUsage + ) + return GenerateTemplateFromCSRPEMWithUsages(csrPEM, duration, isCA, ku, eku) +} + +func GenerateTemplateFromCSRPEMWithUsages(csrPEM []byte, duration time.Duration, isCA bool, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage) (*x509.Certificate, error) { + block, _ := pem.Decode(csrPEM) + if block == nil { + return nil, errors.New("failed to decode csr") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, err + } + + if err := csr.CheckSignature(); err != nil { + return nil, err + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + PublicKey: csr.PublicKey, + IsCA: isCA, + Subject: csr.Subject, + RawSubject: csr.RawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(duration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + URIs: csr.URIs, + }, nil +} + +// SignCertificate returns a signed *x509.Certificate given a template +// *x509.Certificate crt and an issuer. +// publicKey is the public key of the signee, and signerKey is the private +// key of the signer. +// It returns a PEM encoded copy of the Certificate as well as a *x509.Certificate +// which can be used for reading the encoded values. +func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey interface{}) ([]byte, *x509.Certificate, error) { + derBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, signerKey) + + if err != nil { + return nil, nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, nil, fmt.Errorf("error decoding DER certificate bytes: %s", err.Error()) + } + + pemBytes := bytes.NewBuffer([]byte{}) + err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return nil, nil, fmt.Errorf("error encoding certificate PEM: %s", err.Error()) + } + + return pemBytes.Bytes(), cert, err +} + +// // SignCSRTemplate signs a certificate template usually based upon a CSR. This +// // function expects all fields to be present in the certificate template, +// // including it's public key. +// // It returns the PEM bundle containing certificate data and the CA data, encoded in PEM format. +// func SignCSRTemplate(caCerts []*x509.Certificate, caKey crypto.Signer, template *x509.Certificate) (PEMBundle, error) { +// if len(caCerts) == 0 { +// return PEMBundle{}, errors.New("no CA certificates given to sign CSR template") +// } + +// issuingCACert := caCerts[0] + +// _, cert, err := SignCertificate(template, issuingCACert, template.PublicKey, caKey) +// if err != nil { +// return PEMBundle{}, err +// } + +// bundle, err := ParseSingleCertificateChain(append(caCerts, cert)) +// if err != nil { +// return PEMBundle{}, err +// } + +// return bundle, nil +// } + +// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template. +// It returns a DER encoded signed CSR. +func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) { + derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + return derBytes, nil +} + +// EncodeX509 will encode a single *x509.Certificate into PEM format. +func EncodeX509(cert *x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + + return caPem.Bytes(), nil +} + +// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain. +// Self-signed certificates are not included as per +// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 +// Certificates are output in the order they're given; if the input is not ordered +// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid +// for use in TLS. +func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + for _, cert := range certs { + if cert == nil { + continue + } + + if cert.CheckSignatureFrom(cert) == nil { + // Don't include self-signed certificate + continue + } + + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return caPem.Bytes(), nil +} + +// SignatureAlgorithm will determine the appropriate signature algorithm for +// the given certificate. +// Adapted from https://github.com/cloudflare/cfssl/blob/master/csr/csr.go#L102 +func SignatureAlgorithm(crt *v1.Certificate) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + var sigAlgo x509.SignatureAlgorithm + var pubKeyAlgo x509.PublicKeyAlgorithm + var specAlgorithm v1.PrivateKeyAlgorithm + if crt.Spec.PrivateKey != nil { + specAlgorithm = crt.Spec.PrivateKey.Algorithm + } + switch specAlgorithm { + case v1.PrivateKeyAlgorithm(""): + // If keyAlgorithm is not specified, we default to rsa with keysize 2048 + pubKeyAlgo = x509.RSA + sigAlgo = x509.SHA256WithRSA + case v1.RSAKeyAlgorithm: + pubKeyAlgo = x509.RSA + switch { + case crt.Spec.PrivateKey.Size >= 4096: + sigAlgo = x509.SHA512WithRSA + case crt.Spec.PrivateKey.Size >= 3072: + sigAlgo = x509.SHA384WithRSA + case crt.Spec.PrivateKey.Size >= 2048: + sigAlgo = x509.SHA256WithRSA + // 0 == not set + case crt.Spec.PrivateKey.Size == 0: + sigAlgo = x509.SHA256WithRSA + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported rsa keysize specified: %d. min keysize %d", crt.Spec.PrivateKey.Size, MinRSAKeySize) + } + case v1.Ed25519KeyAlgorithm: + pubKeyAlgo = x509.Ed25519 + sigAlgo = x509.PureEd25519 + case v1.ECDSAKeyAlgorithm: + pubKeyAlgo = x509.ECDSA + switch crt.Spec.PrivateKey.Size { + case 521: + sigAlgo = x509.ECDSAWithSHA512 + case 384: + sigAlgo = x509.ECDSAWithSHA384 + case 256: + sigAlgo = x509.ECDSAWithSHA256 + case 0: + sigAlgo = x509.ECDSAWithSHA256 + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported ecdsa keysize specified: %d", crt.Spec.PrivateKey.Size) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported algorithm specified: %s. should be either 'ecdsa' or 'rsa", crt.Spec.PrivateKey.Algorithm) + } + return pubKeyAlgo, sigAlgo, nil +} + +func extractCommonName(spec v1.CertificateSpec) (string, error) { + var commonName = spec.CommonName + if isLiteralCertificateSubjectEnabled() && len(spec.LiteralSubject) > 0 { + commonName = "" + sequence, err := ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return "", err + } + + for _, rdns := range sequence { + for _, atv := range rdns { + if atv.Type.Equal(OIDConstants.CommonName) { + if str, ok := atv.Value.(string); ok { + commonName = str + } + } + } + } + } + + return commonName, nil + +} + +func isLiteralCertificateSubjectEnabled() bool { + return false + //return utilfeature.DefaultFeatureGate.Enabled(feature.LiteralCertificateSubject) +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/pkg/util/kube/pki.go b/pkg/util/kube/pki.go new file mode 100644 index 0000000..6f8055c --- /dev/null +++ b/pkg/util/kube/pki.go @@ -0,0 +1,43 @@ +package kube + +import ( + "context" + "crypto" + + corev1 "k8s.io/api/core/v1" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corelisters "k8s.io/client-go/listers/core/v1" +) + +func SecretTLSKey(ctx context.Context, secretLister corelisters.SecretLister, namespace, name string) (crypto.Signer, error) { + return SecretTLSKeyRef(ctx, secretLister, namespace, name, corev1.TLSPrivateKeyKey) +} + +//SecretTLSKeyRef will fetch the key from the secret. +func SecretTLSKeyRef(ctx context.Context, secretLister corelisters.SecretLister, namespace, name, keyName string) (crypto.Signer, error) { + secret, err := secretLister.Secrets(namespace).Get(name) + if err != nil { + return nil, err + } + + key, _, err := ParseTLSKeyFromSecret(secret, keyName) + if err != nil { + return nil, err + } + return key, nil +} + +func ParseTLSKeyFromSecret(secret *corev1.Secret, keyName string) (crypto.Signer, []byte, error) { + keyBytes, ok := secret.Data[keyName] + if !ok { + return nil, nil, errors.NewInvalidData("no data for %q in secret '%s/%s'", keyName, secret.Namespace, secret.Name) + } + + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return nil, keyBytes, errors.NewInvalidData(err.Error()) + } + return key, keyBytes, nil +} diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index e29bd42..1920afa 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -1,46 +1,75 @@ package pki import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" + "fmt" + "math/big" "net" "net/url" "strings" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" ) -// URLsFromString parses the urls from the string array -func URLsFromString(urlStrs []string) ([]*url.URL, error) { +func IPAddressesForCertificate(crt *v1.Certificate) []net.IP { + var ipAddresses []net.IP + var ip net.IP + for _, ipName := range crt.Spec.IPAddresses { + ip = net.ParseIP(ipName) + if ip != nil { + ipAddresses = append(ipAddresses, ip) + } + } + return ipAddresses +} + +func URIsForCertificate(crt *v1.Certificate) ([]*url.URL, error) { + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, fmt.Errorf("failed to parse URIs: %s", err) + } + + return uris, nil +} + +func DNSNamesForCertificate(crt *v1.Certificate) ([]string, error) { + _, err := URLsFromStrings(crt.Spec.DNSNames) + if err != nil { + return nil, fmt.Errorf("failed to parse DNSNames: %s", err) + } + + return crt.Spec.DNSNames, nil +} + +func URLsFromStrings(urlStrs []string) ([]*url.URL, error) { var urls []*url.URL var errs []string + for _, urlStr := range urlStrs { url, err := url.Parse(urlStr) if err != nil { errs = append(errs, err.Error()) continue } + urls = append(urls, url) } if len(errs) > 0 { return nil, errors.New(strings.Join(errs, ", ")) } + return urls, nil } -// URLsToString converts the array of *url.URL object to the string array -func URLsToString(urls []*url.URL) []string { - var urlStrs []string - for _, url := range urls { - if urls == nil { - panic("provided url to string is nil") - } - - urlStrs = append(urlStrs, url.String()) - } - - return urlStrs -} - -// IPAddressesToString converts the ip address to the string func IPAddressesToString(ipAddresses []net.IP) []string { var ipNames []string for _, ip := range ipAddresses { @@ -48,3 +77,581 @@ } return ipNames } + +func URLsToString(uris []*url.URL) []string { + var uriStrs []string + for _, uri := range uris { + if uri == nil { + panic("provided uri to string is nil") + } + + uriStrs = append(uriStrs, uri.String()) + } + + return uriStrs +} + +func removeDuplicates(in []string) []string { + var found []string +Outer: + for _, i := range in { + for _, i2 := range found { + if i2 == i { + continue Outer + } + } + found = append(found, i) + } + return found +} + +// OrganizationForCertificate will return the Organization to set for the +// Certificate resource. +// If an Organization is not specifically set, a default will be used. +func OrganizationForCertificate(crt *v1.Certificate) []string { + if crt.Spec.Subject == nil { + return nil + } + return crt.Spec.Subject.Organizations +} + +// SubjectForCertificate will return the Subject from the Certificate resource or an empty one if it is not set +func SubjectForCertificate(crt *v1.Certificate) v1.X509Subject { + if crt.Spec.Subject == nil { + return v1.X509Subject{} + } + + return *crt.Spec.Subject +} + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) + +func BuildKeyUsages(usages []v1.KeyUsage, isCA bool) (ku x509.KeyUsage, eku []x509.ExtKeyUsage, err error) { + var unk []v1.KeyUsage + if isCA { + ku |= x509.KeyUsageCertSign + } + if len(usages) == 0 { + usages = append(usages, v1.DefaultKeyUsages()...) + } + for _, u := range usages { + if kuse, ok := apiutil.KeyUsageType(u); ok { + ku |= kuse + } else if ekuse, ok := apiutil.ExtKeyUsageType(u); ok { + eku = append(eku, ekuse) + } else { + unk = append(unk, u) + } + } + if len(unk) > 0 { + err = fmt.Errorf("unknown key usages: %v", unk) + } + return +} + +func BuildCertManagerKeyUsages(ku x509.KeyUsage, eku []x509.ExtKeyUsage) []v1.KeyUsage { + usages := apiutil.KeyUsageStrings(ku) + usages = append(usages, apiutil.ExtKeyUsageStrings(eku)...) + + return usages +} + +// GenerateCSR will generate a new *x509.CertificateRequest template to be used +// by issuers that utilise CSRs to obtain Certificates. +// The CSR will not be signed, and should be passed to either EncodeCSR or +// to the x509.CreateCertificateRequest function. +func GenerateCSR(crt *v1.Certificate) (*x509.CertificateRequest, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + iPAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + + dnsNames, err := DNSNamesForCertificate(crt) + if err != nil { + return nil, err + } + + uriNames, err := URIsForCertificate(crt) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 && len(crt.Spec.EmailAddresses) == 0 && len(crt.Spec.IPAddresses) == 0 { + return nil, fmt.Errorf("no common name, DNS name, URI SAN, or Email SAN specified on certificate") + } + + pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + // var extraExtensions []pkix.Extension + // if crt.Spec.EncodeUsagesInRequest == nil || *crt.Spec.EncodeUsagesInRequest { + // extraExtensions, err = buildKeyUsagesExtensionsForCertificate(crt) + // if err != nil { + // return nil, err + // } + // } + + // if utilfeature.DefaultFeatureGate.Enabled(feature.UseCertificateRequestBasicConstraints) { + // extension, err := buildBasicConstraintsExtensionsForCertificate(crt.Spec.IsCA) + // if err != nil { + // return nil, err + // } + // extraExtensions = append(extraExtensions, extension) + // } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + RawSubject: rawSubject, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + //ExtraExtensions: extraExtensions, + }, nil + } else { + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + // ExtraExtensions: extraExtensions, + }, nil + } + +} + +// func buildKeyUsagesExtensionsForCertificate(crt *v1.Certificate) ([]pkix.Extension, error) { +// ku, ekus, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) +// if err != nil { +// return nil, fmt.Errorf("failed to build key usages: %w", err) +// } + +// usage, err := buildASN1KeyUsageRequest(ku) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode usages: %w", err) +// } +// asn1ExtendedUsages := []asn1.ObjectIdentifier{} +// for _, eku := range ekus { +// if oid, ok := OIDFromExtKeyUsage(eku); ok { +// asn1ExtendedUsages = append(asn1ExtendedUsages, oid) +// } +// } + +// extraExtensions := []pkix.Extension{usage} +// if len(ekus) > 0 { +// extendedUsage := pkix.Extension{ +// Id: OIDExtensionExtendedKeyUsage, +// } +// extendedUsage.Value, err = asn1.Marshal(asn1ExtendedUsages) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode extended usages: %w", err) +// } + +// extraExtensions = append(extraExtensions, extendedUsage) +// } +// return extraExtensions, nil +// } + +// func buildBasicConstraintsExtensionsForCertificate(isCA bool) (pkix.Extension, error) { + +// basicConstraints := pkix.Extension{ +// Id: OIDExtensionBasicConstraints, +// } + +// constraint := struct { +// IsCA bool +// }{ +// IsCA: isCA, +// } + +// var err error +// basicConstraints.Value, err = asn1.Marshal(constraint) +// if err != nil { +// return pkix.Extension{}, err +// } + +// return basicConstraints, nil +// } + +// GenerateTemplate will create a x509.Certificate for the given Certificate resource. +// This should create a Certificate template that is equivalent to the CertificateRequest +// generated by GenerateCSR. +// The PublicKey field must be populated by the caller. +func GenerateTemplate(crt *v1.Certificate) (*x509.Certificate, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + dnsNames := crt.Spec.DNSNames + ipAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, err + } + keyUsages, extKeyUsages, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(ipAddresses) == 0 && len(uris) == 0 && len(crt.Spec.EmailAddresses) == 0 { + return nil, fmt.Errorf("no common name or subject alt names requested on certificate") + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + certDuration := apiutil.DefaultCertDuration(crt.Spec.Duration) + + pubKeyAlgo, _, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + RawSubject: rawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } else { + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } +} + +// GenerateTemplate will create a x509.Certificate for the given +// CertificateRequest resource +func GenerateTemplateFromCertificateRequest(cr *v1.CertificateRequest) (*x509.Certificate, error) { + certDuration := apiutil.DefaultCertDuration(cr.Spec.Duration) + keyUsage, extKeyUsage, err := BuildKeyUsages(cr.Spec.Usages, cr.Spec.IsCA) + if err != nil { + return nil, err + } + return GenerateTemplateFromCSRPEMWithUsages(cr.Spec.Request, certDuration, cr.Spec.IsCA, keyUsage, extKeyUsage) +} + +func GenerateTemplateFromCSRPEM(csrPEM []byte, duration time.Duration, isCA bool) (*x509.Certificate, error) { + var ( + ku x509.KeyUsage + eku []x509.ExtKeyUsage + ) + return GenerateTemplateFromCSRPEMWithUsages(csrPEM, duration, isCA, ku, eku) +} + +func GenerateTemplateFromCSRPEMWithUsages(csrPEM []byte, duration time.Duration, isCA bool, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage) (*x509.Certificate, error) { + block, _ := pem.Decode(csrPEM) + if block == nil { + return nil, errors.New("failed to decode csr") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, err + } + + if err := csr.CheckSignature(); err != nil { + return nil, err + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + PublicKey: csr.PublicKey, + IsCA: isCA, + Subject: csr.Subject, + RawSubject: csr.RawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(duration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + URIs: csr.URIs, + }, nil +} + +// SignCertificate returns a signed *x509.Certificate given a template +// *x509.Certificate crt and an issuer. +// publicKey is the public key of the signee, and signerKey is the private +// key of the signer. +// It returns a PEM encoded copy of the Certificate as well as a *x509.Certificate +// which can be used for reading the encoded values. +func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey interface{}) ([]byte, *x509.Certificate, error) { + derBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, signerKey) + + if err != nil { + return nil, nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, nil, fmt.Errorf("error decoding DER certificate bytes: %s", err.Error()) + } + + pemBytes := bytes.NewBuffer([]byte{}) + err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return nil, nil, fmt.Errorf("error encoding certificate PEM: %s", err.Error()) + } + + return pemBytes.Bytes(), cert, err +} + +// // SignCSRTemplate signs a certificate template usually based upon a CSR. This +// // function expects all fields to be present in the certificate template, +// // including it's public key. +// // It returns the PEM bundle containing certificate data and the CA data, encoded in PEM format. +// func SignCSRTemplate(caCerts []*x509.Certificate, caKey crypto.Signer, template *x509.Certificate) (PEMBundle, error) { +// if len(caCerts) == 0 { +// return PEMBundle{}, errors.New("no CA certificates given to sign CSR template") +// } + +// issuingCACert := caCerts[0] + +// _, cert, err := SignCertificate(template, issuingCACert, template.PublicKey, caKey) +// if err != nil { +// return PEMBundle{}, err +// } + +// bundle, err := ParseSingleCertificateChain(append(caCerts, cert)) +// if err != nil { +// return PEMBundle{}, err +// } + +// return bundle, nil +// } + +// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template. +// It returns a DER encoded signed CSR. +func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) { + derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + return derBytes, nil +} + +// EncodeX509 will encode a single *x509.Certificate into PEM format. +func EncodeX509(cert *x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + + return caPem.Bytes(), nil +} + +// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain. +// Self-signed certificates are not included as per +// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 +// Certificates are output in the order they're given; if the input is not ordered +// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid +// for use in TLS. +func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + for _, cert := range certs { + if cert == nil { + continue + } + + if cert.CheckSignatureFrom(cert) == nil { + // Don't include self-signed certificate + continue + } + + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return caPem.Bytes(), nil +} + +// SignatureAlgorithm will determine the appropriate signature algorithm for +// the given certificate. +// Adapted from https://github.com/cloudflare/cfssl/blob/master/csr/csr.go#L102 +func SignatureAlgorithm(crt *v1.Certificate) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + var sigAlgo x509.SignatureAlgorithm + var pubKeyAlgo x509.PublicKeyAlgorithm + var specAlgorithm v1.PrivateKeyAlgorithm + if crt.Spec.PrivateKey != nil { + specAlgorithm = crt.Spec.PrivateKey.Algorithm + } + switch specAlgorithm { + case v1.PrivateKeyAlgorithm(""): + // If keyAlgorithm is not specified, we default to rsa with keysize 2048 + pubKeyAlgo = x509.RSA + sigAlgo = x509.SHA256WithRSA + case v1.RSAKeyAlgorithm: + pubKeyAlgo = x509.RSA + switch { + case crt.Spec.PrivateKey.Size >= 4096: + sigAlgo = x509.SHA512WithRSA + case crt.Spec.PrivateKey.Size >= 3072: + sigAlgo = x509.SHA384WithRSA + case crt.Spec.PrivateKey.Size >= 2048: + sigAlgo = x509.SHA256WithRSA + // 0 == not set + case crt.Spec.PrivateKey.Size == 0: + sigAlgo = x509.SHA256WithRSA + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported rsa keysize specified: %d. min keysize %d", crt.Spec.PrivateKey.Size, MinRSAKeySize) + } + case v1.Ed25519KeyAlgorithm: + pubKeyAlgo = x509.Ed25519 + sigAlgo = x509.PureEd25519 + case v1.ECDSAKeyAlgorithm: + pubKeyAlgo = x509.ECDSA + switch crt.Spec.PrivateKey.Size { + case 521: + sigAlgo = x509.ECDSAWithSHA512 + case 384: + sigAlgo = x509.ECDSAWithSHA384 + case 256: + sigAlgo = x509.ECDSAWithSHA256 + case 0: + sigAlgo = x509.ECDSAWithSHA256 + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported ecdsa keysize specified: %d", crt.Spec.PrivateKey.Size) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported algorithm specified: %s. should be either 'ecdsa' or 'rsa", crt.Spec.PrivateKey.Algorithm) + } + return pubKeyAlgo, sigAlgo, nil +} + +func extractCommonName(spec v1.CertificateSpec) (string, error) { + var commonName = spec.CommonName + if isLiteralCertificateSubjectEnabled() && len(spec.LiteralSubject) > 0 { + commonName = "" + sequence, err := ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return "", err + } + + for _, rdns := range sequence { + for _, atv := range rdns { + if atv.Type.Equal(OIDConstants.CommonName) { + if str, ok := atv.Value.(string); ok { + commonName = str + } + } + } + } + } + + return commonName, nil + +} + +func isLiteralCertificateSubjectEnabled() bool { + return false + //return utilfeature.DefaultFeatureGate.Enabled(feature.LiteralCertificateSubject) +} diff --git a/pkg/util/pki/generate.go b/pkg/util/pki/generate.go index 911a33c..de1395b 100644 --- a/pkg/util/pki/generate.go +++ b/pkg/util/pki/generate.go @@ -4,12 +4,32 @@ "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +const ( + // MinRSAKeySize is the minimum RSA keysize allowed to be generated by the + // generator functions in this package. + MinRSAKeySize = 2048 + + // MaxRSAKeySize is the maximum RSA keysize allowed to be generated by the + // generator functions in this package. + MaxRSAKeySize = 8192 + + // ECCurve256 represents a secp256r1 / prime256v1 / NIST P-256 ECDSA key. + ECCurve256 = 256 + // ECCurve384 represents a secp384r1 / NIST P-384 ECDSA key. + ECCurve384 = 384 + // ECCurve521 represents a secp521r1 / NIST P-521 ECDSA key. + ECCurve521 = 521 ) // EncodePrivateKey will encode a given crypto.PrivateKey by first inspecting @@ -63,3 +83,119 @@ block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes} return pem.EncodeToMemory(block), nil } + +// PublicKeyMatchesCSR can be used to verify the given public key matches the +// public key in the given x509.CertificateRequest. +// Returns false and no error if the given public key is *not* the same as the CSR's key +// Returns true and no error if the given public key *is* the same as the CSR's key +// Returns an error if the CSR's key type cannot be determined (i.e. non RSA/ECDSA keys) +func PublicKeyMatchesCSR(check crypto.PublicKey, csr *x509.CertificateRequest) (bool, error) { + return PublicKeysEqual(csr.PublicKey, check) +} + +// PublicKeysEqual compares two given public keys for equality. +// The definition of "equality" depends on the type of the public keys. +// Returns true if the keys are the same, false if they differ or an error if +// the key type of `a` cannot be determined. +func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) { + switch pub := a.(type) { + case *rsa.PublicKey: + return pub.Equal(b), nil + case *ecdsa.PublicKey: + return pub.Equal(b), nil + case ed25519.PublicKey: + return pub.Equal(b), nil + default: + return false, fmt.Errorf("unrecognised public key type: %T", a) + } +} + +// GeneratePrivateKeyForCertificate will generate a private key suitable for +// the provided cert-manager Certificate resource, taking into account the +// parameters on the provided resource. +// The returned key will either be RSA or ECDSA. +func GeneratePrivateKeyForCertificate(crt *v1.Certificate) (crypto.Signer, error) { + crt = crt.DeepCopy() + if crt.Spec.PrivateKey == nil { + crt.Spec.PrivateKey = &v1.CertificatePrivateKey{} + } + switch crt.Spec.PrivateKey.Algorithm { + case v1.PrivateKeyAlgorithm(""), v1.RSAKeyAlgorithm: + keySize := MinRSAKeySize + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateRSAPrivateKey(keySize) + case v1.ECDSAKeyAlgorithm: + keySize := ECCurve256 + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateECPrivateKey(keySize) + case v1.Ed25519KeyAlgorithm: + return GenerateEd25519PrivateKey() + default: + return nil, fmt.Errorf("unsupported private key algorithm specified: %s", crt.Spec.PrivateKey.Algorithm) + } +} + +// GenerateRSAPrivateKey will generate a RSA private key of the given size. +// It places restrictions on the minimum and maximum RSA keysize. +func GenerateRSAPrivateKey(keySize int) (*rsa.PrivateKey, error) { + // Do not allow keySize < 2048 + // https://en.wikipedia.org/wiki/Key_size#cite_note-twirl-14 + if keySize < MinRSAKeySize { + return nil, fmt.Errorf("weak rsa key size specified: %d. minimum key size: %d", keySize, MinRSAKeySize) + } + if keySize > MaxRSAKeySize { + return nil, fmt.Errorf("rsa key size specified too big: %d. maximum key size: %d", keySize, MaxRSAKeySize) + } + + return rsa.GenerateKey(rand.Reader, keySize) +} + +// GenerateECPrivateKey will generate an ECDSA private key of the given size. +// It can be used to generate 256, 384 and 521 sized keys. +func GenerateECPrivateKey(keySize int) (*ecdsa.PrivateKey, error) { + var ecCurve elliptic.Curve + + switch keySize { + case ECCurve256: + ecCurve = elliptic.P256() + case ECCurve384: + ecCurve = elliptic.P384() + case ECCurve521: + ecCurve = elliptic.P521() + default: + return nil, fmt.Errorf("unsupported ecdsa key size specified: %d", keySize) + } + + return ecdsa.GenerateKey(ecCurve, rand.Reader) +} + +// GenerateEd25519PrivateKey will generate an Ed25519 private key +func GenerateEd25519PrivateKey() (ed25519.PrivateKey, error) { + + _, prvkey, err := ed25519.GenerateKey(rand.Reader) + + return prvkey, err +} + +// PublicKeyForPrivateKey will return the crypto.PublicKey for the given +// crypto.PrivateKey. It only supports RSA and ECDSA keys. +func PublicKeyForPrivateKey(pk crypto.PrivateKey) (crypto.PublicKey, error) { + switch k := pk.(type) { + case *rsa.PrivateKey: + return k.Public(), nil + case *ecdsa.PrivateKey: + return k.Public(), nil + case ed25519.PrivateKey: + return k.Public(), nil + default: + return nil, fmt.Errorf("unknown private key type: %T", pk) + } +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/pkg/util/kube/pki.go b/pkg/util/kube/pki.go new file mode 100644 index 0000000..6f8055c --- /dev/null +++ b/pkg/util/kube/pki.go @@ -0,0 +1,43 @@ +package kube + +import ( + "context" + "crypto" + + corev1 "k8s.io/api/core/v1" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corelisters "k8s.io/client-go/listers/core/v1" +) + +func SecretTLSKey(ctx context.Context, secretLister corelisters.SecretLister, namespace, name string) (crypto.Signer, error) { + return SecretTLSKeyRef(ctx, secretLister, namespace, name, corev1.TLSPrivateKeyKey) +} + +//SecretTLSKeyRef will fetch the key from the secret. +func SecretTLSKeyRef(ctx context.Context, secretLister corelisters.SecretLister, namespace, name, keyName string) (crypto.Signer, error) { + secret, err := secretLister.Secrets(namespace).Get(name) + if err != nil { + return nil, err + } + + key, _, err := ParseTLSKeyFromSecret(secret, keyName) + if err != nil { + return nil, err + } + return key, nil +} + +func ParseTLSKeyFromSecret(secret *corev1.Secret, keyName string) (crypto.Signer, []byte, error) { + keyBytes, ok := secret.Data[keyName] + if !ok { + return nil, nil, errors.NewInvalidData("no data for %q in secret '%s/%s'", keyName, secret.Namespace, secret.Name) + } + + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return nil, keyBytes, errors.NewInvalidData(err.Error()) + } + return key, keyBytes, nil +} diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index e29bd42..1920afa 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -1,46 +1,75 @@ package pki import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" + "fmt" + "math/big" "net" "net/url" "strings" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" ) -// URLsFromString parses the urls from the string array -func URLsFromString(urlStrs []string) ([]*url.URL, error) { +func IPAddressesForCertificate(crt *v1.Certificate) []net.IP { + var ipAddresses []net.IP + var ip net.IP + for _, ipName := range crt.Spec.IPAddresses { + ip = net.ParseIP(ipName) + if ip != nil { + ipAddresses = append(ipAddresses, ip) + } + } + return ipAddresses +} + +func URIsForCertificate(crt *v1.Certificate) ([]*url.URL, error) { + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, fmt.Errorf("failed to parse URIs: %s", err) + } + + return uris, nil +} + +func DNSNamesForCertificate(crt *v1.Certificate) ([]string, error) { + _, err := URLsFromStrings(crt.Spec.DNSNames) + if err != nil { + return nil, fmt.Errorf("failed to parse DNSNames: %s", err) + } + + return crt.Spec.DNSNames, nil +} + +func URLsFromStrings(urlStrs []string) ([]*url.URL, error) { var urls []*url.URL var errs []string + for _, urlStr := range urlStrs { url, err := url.Parse(urlStr) if err != nil { errs = append(errs, err.Error()) continue } + urls = append(urls, url) } if len(errs) > 0 { return nil, errors.New(strings.Join(errs, ", ")) } + return urls, nil } -// URLsToString converts the array of *url.URL object to the string array -func URLsToString(urls []*url.URL) []string { - var urlStrs []string - for _, url := range urls { - if urls == nil { - panic("provided url to string is nil") - } - - urlStrs = append(urlStrs, url.String()) - } - - return urlStrs -} - -// IPAddressesToString converts the ip address to the string func IPAddressesToString(ipAddresses []net.IP) []string { var ipNames []string for _, ip := range ipAddresses { @@ -48,3 +77,581 @@ } return ipNames } + +func URLsToString(uris []*url.URL) []string { + var uriStrs []string + for _, uri := range uris { + if uri == nil { + panic("provided uri to string is nil") + } + + uriStrs = append(uriStrs, uri.String()) + } + + return uriStrs +} + +func removeDuplicates(in []string) []string { + var found []string +Outer: + for _, i := range in { + for _, i2 := range found { + if i2 == i { + continue Outer + } + } + found = append(found, i) + } + return found +} + +// OrganizationForCertificate will return the Organization to set for the +// Certificate resource. +// If an Organization is not specifically set, a default will be used. +func OrganizationForCertificate(crt *v1.Certificate) []string { + if crt.Spec.Subject == nil { + return nil + } + return crt.Spec.Subject.Organizations +} + +// SubjectForCertificate will return the Subject from the Certificate resource or an empty one if it is not set +func SubjectForCertificate(crt *v1.Certificate) v1.X509Subject { + if crt.Spec.Subject == nil { + return v1.X509Subject{} + } + + return *crt.Spec.Subject +} + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) + +func BuildKeyUsages(usages []v1.KeyUsage, isCA bool) (ku x509.KeyUsage, eku []x509.ExtKeyUsage, err error) { + var unk []v1.KeyUsage + if isCA { + ku |= x509.KeyUsageCertSign + } + if len(usages) == 0 { + usages = append(usages, v1.DefaultKeyUsages()...) + } + for _, u := range usages { + if kuse, ok := apiutil.KeyUsageType(u); ok { + ku |= kuse + } else if ekuse, ok := apiutil.ExtKeyUsageType(u); ok { + eku = append(eku, ekuse) + } else { + unk = append(unk, u) + } + } + if len(unk) > 0 { + err = fmt.Errorf("unknown key usages: %v", unk) + } + return +} + +func BuildCertManagerKeyUsages(ku x509.KeyUsage, eku []x509.ExtKeyUsage) []v1.KeyUsage { + usages := apiutil.KeyUsageStrings(ku) + usages = append(usages, apiutil.ExtKeyUsageStrings(eku)...) + + return usages +} + +// GenerateCSR will generate a new *x509.CertificateRequest template to be used +// by issuers that utilise CSRs to obtain Certificates. +// The CSR will not be signed, and should be passed to either EncodeCSR or +// to the x509.CreateCertificateRequest function. +func GenerateCSR(crt *v1.Certificate) (*x509.CertificateRequest, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + iPAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + + dnsNames, err := DNSNamesForCertificate(crt) + if err != nil { + return nil, err + } + + uriNames, err := URIsForCertificate(crt) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 && len(crt.Spec.EmailAddresses) == 0 && len(crt.Spec.IPAddresses) == 0 { + return nil, fmt.Errorf("no common name, DNS name, URI SAN, or Email SAN specified on certificate") + } + + pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + // var extraExtensions []pkix.Extension + // if crt.Spec.EncodeUsagesInRequest == nil || *crt.Spec.EncodeUsagesInRequest { + // extraExtensions, err = buildKeyUsagesExtensionsForCertificate(crt) + // if err != nil { + // return nil, err + // } + // } + + // if utilfeature.DefaultFeatureGate.Enabled(feature.UseCertificateRequestBasicConstraints) { + // extension, err := buildBasicConstraintsExtensionsForCertificate(crt.Spec.IsCA) + // if err != nil { + // return nil, err + // } + // extraExtensions = append(extraExtensions, extension) + // } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + RawSubject: rawSubject, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + //ExtraExtensions: extraExtensions, + }, nil + } else { + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + // ExtraExtensions: extraExtensions, + }, nil + } + +} + +// func buildKeyUsagesExtensionsForCertificate(crt *v1.Certificate) ([]pkix.Extension, error) { +// ku, ekus, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) +// if err != nil { +// return nil, fmt.Errorf("failed to build key usages: %w", err) +// } + +// usage, err := buildASN1KeyUsageRequest(ku) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode usages: %w", err) +// } +// asn1ExtendedUsages := []asn1.ObjectIdentifier{} +// for _, eku := range ekus { +// if oid, ok := OIDFromExtKeyUsage(eku); ok { +// asn1ExtendedUsages = append(asn1ExtendedUsages, oid) +// } +// } + +// extraExtensions := []pkix.Extension{usage} +// if len(ekus) > 0 { +// extendedUsage := pkix.Extension{ +// Id: OIDExtensionExtendedKeyUsage, +// } +// extendedUsage.Value, err = asn1.Marshal(asn1ExtendedUsages) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode extended usages: %w", err) +// } + +// extraExtensions = append(extraExtensions, extendedUsage) +// } +// return extraExtensions, nil +// } + +// func buildBasicConstraintsExtensionsForCertificate(isCA bool) (pkix.Extension, error) { + +// basicConstraints := pkix.Extension{ +// Id: OIDExtensionBasicConstraints, +// } + +// constraint := struct { +// IsCA bool +// }{ +// IsCA: isCA, +// } + +// var err error +// basicConstraints.Value, err = asn1.Marshal(constraint) +// if err != nil { +// return pkix.Extension{}, err +// } + +// return basicConstraints, nil +// } + +// GenerateTemplate will create a x509.Certificate for the given Certificate resource. +// This should create a Certificate template that is equivalent to the CertificateRequest +// generated by GenerateCSR. +// The PublicKey field must be populated by the caller. +func GenerateTemplate(crt *v1.Certificate) (*x509.Certificate, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + dnsNames := crt.Spec.DNSNames + ipAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, err + } + keyUsages, extKeyUsages, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(ipAddresses) == 0 && len(uris) == 0 && len(crt.Spec.EmailAddresses) == 0 { + return nil, fmt.Errorf("no common name or subject alt names requested on certificate") + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + certDuration := apiutil.DefaultCertDuration(crt.Spec.Duration) + + pubKeyAlgo, _, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + RawSubject: rawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } else { + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } +} + +// GenerateTemplate will create a x509.Certificate for the given +// CertificateRequest resource +func GenerateTemplateFromCertificateRequest(cr *v1.CertificateRequest) (*x509.Certificate, error) { + certDuration := apiutil.DefaultCertDuration(cr.Spec.Duration) + keyUsage, extKeyUsage, err := BuildKeyUsages(cr.Spec.Usages, cr.Spec.IsCA) + if err != nil { + return nil, err + } + return GenerateTemplateFromCSRPEMWithUsages(cr.Spec.Request, certDuration, cr.Spec.IsCA, keyUsage, extKeyUsage) +} + +func GenerateTemplateFromCSRPEM(csrPEM []byte, duration time.Duration, isCA bool) (*x509.Certificate, error) { + var ( + ku x509.KeyUsage + eku []x509.ExtKeyUsage + ) + return GenerateTemplateFromCSRPEMWithUsages(csrPEM, duration, isCA, ku, eku) +} + +func GenerateTemplateFromCSRPEMWithUsages(csrPEM []byte, duration time.Duration, isCA bool, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage) (*x509.Certificate, error) { + block, _ := pem.Decode(csrPEM) + if block == nil { + return nil, errors.New("failed to decode csr") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, err + } + + if err := csr.CheckSignature(); err != nil { + return nil, err + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + PublicKey: csr.PublicKey, + IsCA: isCA, + Subject: csr.Subject, + RawSubject: csr.RawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(duration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + URIs: csr.URIs, + }, nil +} + +// SignCertificate returns a signed *x509.Certificate given a template +// *x509.Certificate crt and an issuer. +// publicKey is the public key of the signee, and signerKey is the private +// key of the signer. +// It returns a PEM encoded copy of the Certificate as well as a *x509.Certificate +// which can be used for reading the encoded values. +func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey interface{}) ([]byte, *x509.Certificate, error) { + derBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, signerKey) + + if err != nil { + return nil, nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, nil, fmt.Errorf("error decoding DER certificate bytes: %s", err.Error()) + } + + pemBytes := bytes.NewBuffer([]byte{}) + err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return nil, nil, fmt.Errorf("error encoding certificate PEM: %s", err.Error()) + } + + return pemBytes.Bytes(), cert, err +} + +// // SignCSRTemplate signs a certificate template usually based upon a CSR. This +// // function expects all fields to be present in the certificate template, +// // including it's public key. +// // It returns the PEM bundle containing certificate data and the CA data, encoded in PEM format. +// func SignCSRTemplate(caCerts []*x509.Certificate, caKey crypto.Signer, template *x509.Certificate) (PEMBundle, error) { +// if len(caCerts) == 0 { +// return PEMBundle{}, errors.New("no CA certificates given to sign CSR template") +// } + +// issuingCACert := caCerts[0] + +// _, cert, err := SignCertificate(template, issuingCACert, template.PublicKey, caKey) +// if err != nil { +// return PEMBundle{}, err +// } + +// bundle, err := ParseSingleCertificateChain(append(caCerts, cert)) +// if err != nil { +// return PEMBundle{}, err +// } + +// return bundle, nil +// } + +// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template. +// It returns a DER encoded signed CSR. +func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) { + derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + return derBytes, nil +} + +// EncodeX509 will encode a single *x509.Certificate into PEM format. +func EncodeX509(cert *x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + + return caPem.Bytes(), nil +} + +// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain. +// Self-signed certificates are not included as per +// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 +// Certificates are output in the order they're given; if the input is not ordered +// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid +// for use in TLS. +func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + for _, cert := range certs { + if cert == nil { + continue + } + + if cert.CheckSignatureFrom(cert) == nil { + // Don't include self-signed certificate + continue + } + + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return caPem.Bytes(), nil +} + +// SignatureAlgorithm will determine the appropriate signature algorithm for +// the given certificate. +// Adapted from https://github.com/cloudflare/cfssl/blob/master/csr/csr.go#L102 +func SignatureAlgorithm(crt *v1.Certificate) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + var sigAlgo x509.SignatureAlgorithm + var pubKeyAlgo x509.PublicKeyAlgorithm + var specAlgorithm v1.PrivateKeyAlgorithm + if crt.Spec.PrivateKey != nil { + specAlgorithm = crt.Spec.PrivateKey.Algorithm + } + switch specAlgorithm { + case v1.PrivateKeyAlgorithm(""): + // If keyAlgorithm is not specified, we default to rsa with keysize 2048 + pubKeyAlgo = x509.RSA + sigAlgo = x509.SHA256WithRSA + case v1.RSAKeyAlgorithm: + pubKeyAlgo = x509.RSA + switch { + case crt.Spec.PrivateKey.Size >= 4096: + sigAlgo = x509.SHA512WithRSA + case crt.Spec.PrivateKey.Size >= 3072: + sigAlgo = x509.SHA384WithRSA + case crt.Spec.PrivateKey.Size >= 2048: + sigAlgo = x509.SHA256WithRSA + // 0 == not set + case crt.Spec.PrivateKey.Size == 0: + sigAlgo = x509.SHA256WithRSA + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported rsa keysize specified: %d. min keysize %d", crt.Spec.PrivateKey.Size, MinRSAKeySize) + } + case v1.Ed25519KeyAlgorithm: + pubKeyAlgo = x509.Ed25519 + sigAlgo = x509.PureEd25519 + case v1.ECDSAKeyAlgorithm: + pubKeyAlgo = x509.ECDSA + switch crt.Spec.PrivateKey.Size { + case 521: + sigAlgo = x509.ECDSAWithSHA512 + case 384: + sigAlgo = x509.ECDSAWithSHA384 + case 256: + sigAlgo = x509.ECDSAWithSHA256 + case 0: + sigAlgo = x509.ECDSAWithSHA256 + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported ecdsa keysize specified: %d", crt.Spec.PrivateKey.Size) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported algorithm specified: %s. should be either 'ecdsa' or 'rsa", crt.Spec.PrivateKey.Algorithm) + } + return pubKeyAlgo, sigAlgo, nil +} + +func extractCommonName(spec v1.CertificateSpec) (string, error) { + var commonName = spec.CommonName + if isLiteralCertificateSubjectEnabled() && len(spec.LiteralSubject) > 0 { + commonName = "" + sequence, err := ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return "", err + } + + for _, rdns := range sequence { + for _, atv := range rdns { + if atv.Type.Equal(OIDConstants.CommonName) { + if str, ok := atv.Value.(string); ok { + commonName = str + } + } + } + } + } + + return commonName, nil + +} + +func isLiteralCertificateSubjectEnabled() bool { + return false + //return utilfeature.DefaultFeatureGate.Enabled(feature.LiteralCertificateSubject) +} diff --git a/pkg/util/pki/generate.go b/pkg/util/pki/generate.go index 911a33c..de1395b 100644 --- a/pkg/util/pki/generate.go +++ b/pkg/util/pki/generate.go @@ -4,12 +4,32 @@ "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +const ( + // MinRSAKeySize is the minimum RSA keysize allowed to be generated by the + // generator functions in this package. + MinRSAKeySize = 2048 + + // MaxRSAKeySize is the maximum RSA keysize allowed to be generated by the + // generator functions in this package. + MaxRSAKeySize = 8192 + + // ECCurve256 represents a secp256r1 / prime256v1 / NIST P-256 ECDSA key. + ECCurve256 = 256 + // ECCurve384 represents a secp384r1 / NIST P-384 ECDSA key. + ECCurve384 = 384 + // ECCurve521 represents a secp521r1 / NIST P-521 ECDSA key. + ECCurve521 = 521 ) // EncodePrivateKey will encode a given crypto.PrivateKey by first inspecting @@ -63,3 +83,119 @@ block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes} return pem.EncodeToMemory(block), nil } + +// PublicKeyMatchesCSR can be used to verify the given public key matches the +// public key in the given x509.CertificateRequest. +// Returns false and no error if the given public key is *not* the same as the CSR's key +// Returns true and no error if the given public key *is* the same as the CSR's key +// Returns an error if the CSR's key type cannot be determined (i.e. non RSA/ECDSA keys) +func PublicKeyMatchesCSR(check crypto.PublicKey, csr *x509.CertificateRequest) (bool, error) { + return PublicKeysEqual(csr.PublicKey, check) +} + +// PublicKeysEqual compares two given public keys for equality. +// The definition of "equality" depends on the type of the public keys. +// Returns true if the keys are the same, false if they differ or an error if +// the key type of `a` cannot be determined. +func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) { + switch pub := a.(type) { + case *rsa.PublicKey: + return pub.Equal(b), nil + case *ecdsa.PublicKey: + return pub.Equal(b), nil + case ed25519.PublicKey: + return pub.Equal(b), nil + default: + return false, fmt.Errorf("unrecognised public key type: %T", a) + } +} + +// GeneratePrivateKeyForCertificate will generate a private key suitable for +// the provided cert-manager Certificate resource, taking into account the +// parameters on the provided resource. +// The returned key will either be RSA or ECDSA. +func GeneratePrivateKeyForCertificate(crt *v1.Certificate) (crypto.Signer, error) { + crt = crt.DeepCopy() + if crt.Spec.PrivateKey == nil { + crt.Spec.PrivateKey = &v1.CertificatePrivateKey{} + } + switch crt.Spec.PrivateKey.Algorithm { + case v1.PrivateKeyAlgorithm(""), v1.RSAKeyAlgorithm: + keySize := MinRSAKeySize + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateRSAPrivateKey(keySize) + case v1.ECDSAKeyAlgorithm: + keySize := ECCurve256 + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateECPrivateKey(keySize) + case v1.Ed25519KeyAlgorithm: + return GenerateEd25519PrivateKey() + default: + return nil, fmt.Errorf("unsupported private key algorithm specified: %s", crt.Spec.PrivateKey.Algorithm) + } +} + +// GenerateRSAPrivateKey will generate a RSA private key of the given size. +// It places restrictions on the minimum and maximum RSA keysize. +func GenerateRSAPrivateKey(keySize int) (*rsa.PrivateKey, error) { + // Do not allow keySize < 2048 + // https://en.wikipedia.org/wiki/Key_size#cite_note-twirl-14 + if keySize < MinRSAKeySize { + return nil, fmt.Errorf("weak rsa key size specified: %d. minimum key size: %d", keySize, MinRSAKeySize) + } + if keySize > MaxRSAKeySize { + return nil, fmt.Errorf("rsa key size specified too big: %d. maximum key size: %d", keySize, MaxRSAKeySize) + } + + return rsa.GenerateKey(rand.Reader, keySize) +} + +// GenerateECPrivateKey will generate an ECDSA private key of the given size. +// It can be used to generate 256, 384 and 521 sized keys. +func GenerateECPrivateKey(keySize int) (*ecdsa.PrivateKey, error) { + var ecCurve elliptic.Curve + + switch keySize { + case ECCurve256: + ecCurve = elliptic.P256() + case ECCurve384: + ecCurve = elliptic.P384() + case ECCurve521: + ecCurve = elliptic.P521() + default: + return nil, fmt.Errorf("unsupported ecdsa key size specified: %d", keySize) + } + + return ecdsa.GenerateKey(ecCurve, rand.Reader) +} + +// GenerateEd25519PrivateKey will generate an Ed25519 private key +func GenerateEd25519PrivateKey() (ed25519.PrivateKey, error) { + + _, prvkey, err := ed25519.GenerateKey(rand.Reader) + + return prvkey, err +} + +// PublicKeyForPrivateKey will return the crypto.PublicKey for the given +// crypto.PrivateKey. It only supports RSA and ECDSA keys. +func PublicKeyForPrivateKey(pk crypto.PrivateKey) (crypto.PublicKey, error) { + switch k := pk.(type) { + case *rsa.PrivateKey: + return k.Public(), nil + case *ecdsa.PrivateKey: + return k.Public(), nil + case ed25519.PrivateKey: + return k.Public(), nil + default: + return nil, fmt.Errorf("unknown private key type: %T", pk) + } +} diff --git a/pkg/util/pki/keyusage.go b/pkg/util/pki/keyusage.go new file mode 100644 index 0000000..6404b4d --- /dev/null +++ b/pkg/util/pki/keyusage.go @@ -0,0 +1,135 @@ +package pki + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" +) + +// Copied from x509.go +var ( + OIDExtensionKeyUsage = []int{2, 5, 29, 15} + OIDExtensionExtendedKeyUsage = []int{2, 5, 29, 37} + OIDExtensionBasicConstraints = []int{2, 5, 29, 19} +) + +// RFC 5280, 4.2.1.12 Extended Key Usage +// +// anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 } +// +// id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } +// +// id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } +// id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } +// id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 } +// id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 } +// id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 } +// id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } +var ( + oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} + oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} + oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} + oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} + oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} + oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} + oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} + oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} + oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} + oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} + oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} + oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} + oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} + oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1} +) + +// extKeyUsageOIDs contains the mapping between an ExtKeyUsage and its OID. +var extKeyUsageOIDs = []struct { + extKeyUsage x509.ExtKeyUsage + oid asn1.ObjectIdentifier +}{ + {x509.ExtKeyUsageAny, oidExtKeyUsageAny}, + {x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth}, + {x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth}, + {x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning}, + {x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection}, + {x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem}, + {x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel}, + {x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser}, + {x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping}, + {x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning}, + {x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto}, + {x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto}, + {x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning}, + {x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning}, +} + +// OIDFromExtKeyUsage returns the ASN1 Identifier for a x509.ExtKeyUsage +func OIDFromExtKeyUsage(eku x509.ExtKeyUsage) (oid asn1.ObjectIdentifier, ok bool) { + for _, pair := range extKeyUsageOIDs { + if eku == pair.extKeyUsage { + return pair.oid, true + } + } + return +} + +func ExtKeyUsageFromOID(oid asn1.ObjectIdentifier) (eku x509.ExtKeyUsage, ok bool) { + for _, pair := range extKeyUsageOIDs { + if oid.Equal(pair.oid) { + return pair.extKeyUsage, true + } + } + return +} + +// asn1BitLength returns the bit-length of bitString by considering the +// most-significant bit in a byte to be the "first" bit. This convention +// matches ASN.1, but differs from almost everything else. +func asn1BitLength(bitString []byte) int { + bitLen := len(bitString) * 8 + + for i := range bitString { + b := bitString[len(bitString)-i-1] + + for bit := uint(0); bit < 8; bit++ { + if (b>>bit)&1 == 1 { + return bitLen + } + bitLen-- + } + } + + return 0 +} + +// Copied from x509.go +func reverseBitsInAByte(in byte) byte { + b1 := in>>4 | in<<4 + b2 := b1>>2&0x33 | b1<<2&0xcc + b3 := b2>>1&0x55 | b2<<1&0xaa + return b3 +} + +// Adapted from x509.go +func buildASN1KeyUsageRequest(usage x509.KeyUsage) (pkix.Extension, error) { + OIDExtensionKeyUsage := pkix.Extension{ + Id: OIDExtensionKeyUsage, + } + var a [2]byte + a[0] = reverseBitsInAByte(byte(usage)) + a[1] = reverseBitsInAByte(byte(usage >> 8)) + + l := 1 + if a[1] != 0 { + l = 2 + } + + bitString := a[:l] + var err error + OIDExtensionKeyUsage.Value, err = asn1.Marshal(asn1.BitString{Bytes: bitString, BitLength: asn1BitLength(bitString)}) + if err != nil { + return pkix.Extension{}, err + } + + return OIDExtensionKeyUsage, nil +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/pkg/util/kube/pki.go b/pkg/util/kube/pki.go new file mode 100644 index 0000000..6f8055c --- /dev/null +++ b/pkg/util/kube/pki.go @@ -0,0 +1,43 @@ +package kube + +import ( + "context" + "crypto" + + corev1 "k8s.io/api/core/v1" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corelisters "k8s.io/client-go/listers/core/v1" +) + +func SecretTLSKey(ctx context.Context, secretLister corelisters.SecretLister, namespace, name string) (crypto.Signer, error) { + return SecretTLSKeyRef(ctx, secretLister, namespace, name, corev1.TLSPrivateKeyKey) +} + +//SecretTLSKeyRef will fetch the key from the secret. +func SecretTLSKeyRef(ctx context.Context, secretLister corelisters.SecretLister, namespace, name, keyName string) (crypto.Signer, error) { + secret, err := secretLister.Secrets(namespace).Get(name) + if err != nil { + return nil, err + } + + key, _, err := ParseTLSKeyFromSecret(secret, keyName) + if err != nil { + return nil, err + } + return key, nil +} + +func ParseTLSKeyFromSecret(secret *corev1.Secret, keyName string) (crypto.Signer, []byte, error) { + keyBytes, ok := secret.Data[keyName] + if !ok { + return nil, nil, errors.NewInvalidData("no data for %q in secret '%s/%s'", keyName, secret.Namespace, secret.Name) + } + + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return nil, keyBytes, errors.NewInvalidData(err.Error()) + } + return key, keyBytes, nil +} diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index e29bd42..1920afa 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -1,46 +1,75 @@ package pki import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" + "fmt" + "math/big" "net" "net/url" "strings" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" ) -// URLsFromString parses the urls from the string array -func URLsFromString(urlStrs []string) ([]*url.URL, error) { +func IPAddressesForCertificate(crt *v1.Certificate) []net.IP { + var ipAddresses []net.IP + var ip net.IP + for _, ipName := range crt.Spec.IPAddresses { + ip = net.ParseIP(ipName) + if ip != nil { + ipAddresses = append(ipAddresses, ip) + } + } + return ipAddresses +} + +func URIsForCertificate(crt *v1.Certificate) ([]*url.URL, error) { + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, fmt.Errorf("failed to parse URIs: %s", err) + } + + return uris, nil +} + +func DNSNamesForCertificate(crt *v1.Certificate) ([]string, error) { + _, err := URLsFromStrings(crt.Spec.DNSNames) + if err != nil { + return nil, fmt.Errorf("failed to parse DNSNames: %s", err) + } + + return crt.Spec.DNSNames, nil +} + +func URLsFromStrings(urlStrs []string) ([]*url.URL, error) { var urls []*url.URL var errs []string + for _, urlStr := range urlStrs { url, err := url.Parse(urlStr) if err != nil { errs = append(errs, err.Error()) continue } + urls = append(urls, url) } if len(errs) > 0 { return nil, errors.New(strings.Join(errs, ", ")) } + return urls, nil } -// URLsToString converts the array of *url.URL object to the string array -func URLsToString(urls []*url.URL) []string { - var urlStrs []string - for _, url := range urls { - if urls == nil { - panic("provided url to string is nil") - } - - urlStrs = append(urlStrs, url.String()) - } - - return urlStrs -} - -// IPAddressesToString converts the ip address to the string func IPAddressesToString(ipAddresses []net.IP) []string { var ipNames []string for _, ip := range ipAddresses { @@ -48,3 +77,581 @@ } return ipNames } + +func URLsToString(uris []*url.URL) []string { + var uriStrs []string + for _, uri := range uris { + if uri == nil { + panic("provided uri to string is nil") + } + + uriStrs = append(uriStrs, uri.String()) + } + + return uriStrs +} + +func removeDuplicates(in []string) []string { + var found []string +Outer: + for _, i := range in { + for _, i2 := range found { + if i2 == i { + continue Outer + } + } + found = append(found, i) + } + return found +} + +// OrganizationForCertificate will return the Organization to set for the +// Certificate resource. +// If an Organization is not specifically set, a default will be used. +func OrganizationForCertificate(crt *v1.Certificate) []string { + if crt.Spec.Subject == nil { + return nil + } + return crt.Spec.Subject.Organizations +} + +// SubjectForCertificate will return the Subject from the Certificate resource or an empty one if it is not set +func SubjectForCertificate(crt *v1.Certificate) v1.X509Subject { + if crt.Spec.Subject == nil { + return v1.X509Subject{} + } + + return *crt.Spec.Subject +} + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) + +func BuildKeyUsages(usages []v1.KeyUsage, isCA bool) (ku x509.KeyUsage, eku []x509.ExtKeyUsage, err error) { + var unk []v1.KeyUsage + if isCA { + ku |= x509.KeyUsageCertSign + } + if len(usages) == 0 { + usages = append(usages, v1.DefaultKeyUsages()...) + } + for _, u := range usages { + if kuse, ok := apiutil.KeyUsageType(u); ok { + ku |= kuse + } else if ekuse, ok := apiutil.ExtKeyUsageType(u); ok { + eku = append(eku, ekuse) + } else { + unk = append(unk, u) + } + } + if len(unk) > 0 { + err = fmt.Errorf("unknown key usages: %v", unk) + } + return +} + +func BuildCertManagerKeyUsages(ku x509.KeyUsage, eku []x509.ExtKeyUsage) []v1.KeyUsage { + usages := apiutil.KeyUsageStrings(ku) + usages = append(usages, apiutil.ExtKeyUsageStrings(eku)...) + + return usages +} + +// GenerateCSR will generate a new *x509.CertificateRequest template to be used +// by issuers that utilise CSRs to obtain Certificates. +// The CSR will not be signed, and should be passed to either EncodeCSR or +// to the x509.CreateCertificateRequest function. +func GenerateCSR(crt *v1.Certificate) (*x509.CertificateRequest, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + iPAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + + dnsNames, err := DNSNamesForCertificate(crt) + if err != nil { + return nil, err + } + + uriNames, err := URIsForCertificate(crt) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 && len(crt.Spec.EmailAddresses) == 0 && len(crt.Spec.IPAddresses) == 0 { + return nil, fmt.Errorf("no common name, DNS name, URI SAN, or Email SAN specified on certificate") + } + + pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + // var extraExtensions []pkix.Extension + // if crt.Spec.EncodeUsagesInRequest == nil || *crt.Spec.EncodeUsagesInRequest { + // extraExtensions, err = buildKeyUsagesExtensionsForCertificate(crt) + // if err != nil { + // return nil, err + // } + // } + + // if utilfeature.DefaultFeatureGate.Enabled(feature.UseCertificateRequestBasicConstraints) { + // extension, err := buildBasicConstraintsExtensionsForCertificate(crt.Spec.IsCA) + // if err != nil { + // return nil, err + // } + // extraExtensions = append(extraExtensions, extension) + // } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + RawSubject: rawSubject, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + //ExtraExtensions: extraExtensions, + }, nil + } else { + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + // ExtraExtensions: extraExtensions, + }, nil + } + +} + +// func buildKeyUsagesExtensionsForCertificate(crt *v1.Certificate) ([]pkix.Extension, error) { +// ku, ekus, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) +// if err != nil { +// return nil, fmt.Errorf("failed to build key usages: %w", err) +// } + +// usage, err := buildASN1KeyUsageRequest(ku) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode usages: %w", err) +// } +// asn1ExtendedUsages := []asn1.ObjectIdentifier{} +// for _, eku := range ekus { +// if oid, ok := OIDFromExtKeyUsage(eku); ok { +// asn1ExtendedUsages = append(asn1ExtendedUsages, oid) +// } +// } + +// extraExtensions := []pkix.Extension{usage} +// if len(ekus) > 0 { +// extendedUsage := pkix.Extension{ +// Id: OIDExtensionExtendedKeyUsage, +// } +// extendedUsage.Value, err = asn1.Marshal(asn1ExtendedUsages) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode extended usages: %w", err) +// } + +// extraExtensions = append(extraExtensions, extendedUsage) +// } +// return extraExtensions, nil +// } + +// func buildBasicConstraintsExtensionsForCertificate(isCA bool) (pkix.Extension, error) { + +// basicConstraints := pkix.Extension{ +// Id: OIDExtensionBasicConstraints, +// } + +// constraint := struct { +// IsCA bool +// }{ +// IsCA: isCA, +// } + +// var err error +// basicConstraints.Value, err = asn1.Marshal(constraint) +// if err != nil { +// return pkix.Extension{}, err +// } + +// return basicConstraints, nil +// } + +// GenerateTemplate will create a x509.Certificate for the given Certificate resource. +// This should create a Certificate template that is equivalent to the CertificateRequest +// generated by GenerateCSR. +// The PublicKey field must be populated by the caller. +func GenerateTemplate(crt *v1.Certificate) (*x509.Certificate, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + dnsNames := crt.Spec.DNSNames + ipAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, err + } + keyUsages, extKeyUsages, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(ipAddresses) == 0 && len(uris) == 0 && len(crt.Spec.EmailAddresses) == 0 { + return nil, fmt.Errorf("no common name or subject alt names requested on certificate") + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + certDuration := apiutil.DefaultCertDuration(crt.Spec.Duration) + + pubKeyAlgo, _, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + RawSubject: rawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } else { + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } +} + +// GenerateTemplate will create a x509.Certificate for the given +// CertificateRequest resource +func GenerateTemplateFromCertificateRequest(cr *v1.CertificateRequest) (*x509.Certificate, error) { + certDuration := apiutil.DefaultCertDuration(cr.Spec.Duration) + keyUsage, extKeyUsage, err := BuildKeyUsages(cr.Spec.Usages, cr.Spec.IsCA) + if err != nil { + return nil, err + } + return GenerateTemplateFromCSRPEMWithUsages(cr.Spec.Request, certDuration, cr.Spec.IsCA, keyUsage, extKeyUsage) +} + +func GenerateTemplateFromCSRPEM(csrPEM []byte, duration time.Duration, isCA bool) (*x509.Certificate, error) { + var ( + ku x509.KeyUsage + eku []x509.ExtKeyUsage + ) + return GenerateTemplateFromCSRPEMWithUsages(csrPEM, duration, isCA, ku, eku) +} + +func GenerateTemplateFromCSRPEMWithUsages(csrPEM []byte, duration time.Duration, isCA bool, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage) (*x509.Certificate, error) { + block, _ := pem.Decode(csrPEM) + if block == nil { + return nil, errors.New("failed to decode csr") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, err + } + + if err := csr.CheckSignature(); err != nil { + return nil, err + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + PublicKey: csr.PublicKey, + IsCA: isCA, + Subject: csr.Subject, + RawSubject: csr.RawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(duration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + URIs: csr.URIs, + }, nil +} + +// SignCertificate returns a signed *x509.Certificate given a template +// *x509.Certificate crt and an issuer. +// publicKey is the public key of the signee, and signerKey is the private +// key of the signer. +// It returns a PEM encoded copy of the Certificate as well as a *x509.Certificate +// which can be used for reading the encoded values. +func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey interface{}) ([]byte, *x509.Certificate, error) { + derBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, signerKey) + + if err != nil { + return nil, nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, nil, fmt.Errorf("error decoding DER certificate bytes: %s", err.Error()) + } + + pemBytes := bytes.NewBuffer([]byte{}) + err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return nil, nil, fmt.Errorf("error encoding certificate PEM: %s", err.Error()) + } + + return pemBytes.Bytes(), cert, err +} + +// // SignCSRTemplate signs a certificate template usually based upon a CSR. This +// // function expects all fields to be present in the certificate template, +// // including it's public key. +// // It returns the PEM bundle containing certificate data and the CA data, encoded in PEM format. +// func SignCSRTemplate(caCerts []*x509.Certificate, caKey crypto.Signer, template *x509.Certificate) (PEMBundle, error) { +// if len(caCerts) == 0 { +// return PEMBundle{}, errors.New("no CA certificates given to sign CSR template") +// } + +// issuingCACert := caCerts[0] + +// _, cert, err := SignCertificate(template, issuingCACert, template.PublicKey, caKey) +// if err != nil { +// return PEMBundle{}, err +// } + +// bundle, err := ParseSingleCertificateChain(append(caCerts, cert)) +// if err != nil { +// return PEMBundle{}, err +// } + +// return bundle, nil +// } + +// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template. +// It returns a DER encoded signed CSR. +func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) { + derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + return derBytes, nil +} + +// EncodeX509 will encode a single *x509.Certificate into PEM format. +func EncodeX509(cert *x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + + return caPem.Bytes(), nil +} + +// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain. +// Self-signed certificates are not included as per +// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 +// Certificates are output in the order they're given; if the input is not ordered +// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid +// for use in TLS. +func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + for _, cert := range certs { + if cert == nil { + continue + } + + if cert.CheckSignatureFrom(cert) == nil { + // Don't include self-signed certificate + continue + } + + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return caPem.Bytes(), nil +} + +// SignatureAlgorithm will determine the appropriate signature algorithm for +// the given certificate. +// Adapted from https://github.com/cloudflare/cfssl/blob/master/csr/csr.go#L102 +func SignatureAlgorithm(crt *v1.Certificate) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + var sigAlgo x509.SignatureAlgorithm + var pubKeyAlgo x509.PublicKeyAlgorithm + var specAlgorithm v1.PrivateKeyAlgorithm + if crt.Spec.PrivateKey != nil { + specAlgorithm = crt.Spec.PrivateKey.Algorithm + } + switch specAlgorithm { + case v1.PrivateKeyAlgorithm(""): + // If keyAlgorithm is not specified, we default to rsa with keysize 2048 + pubKeyAlgo = x509.RSA + sigAlgo = x509.SHA256WithRSA + case v1.RSAKeyAlgorithm: + pubKeyAlgo = x509.RSA + switch { + case crt.Spec.PrivateKey.Size >= 4096: + sigAlgo = x509.SHA512WithRSA + case crt.Spec.PrivateKey.Size >= 3072: + sigAlgo = x509.SHA384WithRSA + case crt.Spec.PrivateKey.Size >= 2048: + sigAlgo = x509.SHA256WithRSA + // 0 == not set + case crt.Spec.PrivateKey.Size == 0: + sigAlgo = x509.SHA256WithRSA + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported rsa keysize specified: %d. min keysize %d", crt.Spec.PrivateKey.Size, MinRSAKeySize) + } + case v1.Ed25519KeyAlgorithm: + pubKeyAlgo = x509.Ed25519 + sigAlgo = x509.PureEd25519 + case v1.ECDSAKeyAlgorithm: + pubKeyAlgo = x509.ECDSA + switch crt.Spec.PrivateKey.Size { + case 521: + sigAlgo = x509.ECDSAWithSHA512 + case 384: + sigAlgo = x509.ECDSAWithSHA384 + case 256: + sigAlgo = x509.ECDSAWithSHA256 + case 0: + sigAlgo = x509.ECDSAWithSHA256 + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported ecdsa keysize specified: %d", crt.Spec.PrivateKey.Size) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported algorithm specified: %s. should be either 'ecdsa' or 'rsa", crt.Spec.PrivateKey.Algorithm) + } + return pubKeyAlgo, sigAlgo, nil +} + +func extractCommonName(spec v1.CertificateSpec) (string, error) { + var commonName = spec.CommonName + if isLiteralCertificateSubjectEnabled() && len(spec.LiteralSubject) > 0 { + commonName = "" + sequence, err := ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return "", err + } + + for _, rdns := range sequence { + for _, atv := range rdns { + if atv.Type.Equal(OIDConstants.CommonName) { + if str, ok := atv.Value.(string); ok { + commonName = str + } + } + } + } + } + + return commonName, nil + +} + +func isLiteralCertificateSubjectEnabled() bool { + return false + //return utilfeature.DefaultFeatureGate.Enabled(feature.LiteralCertificateSubject) +} diff --git a/pkg/util/pki/generate.go b/pkg/util/pki/generate.go index 911a33c..de1395b 100644 --- a/pkg/util/pki/generate.go +++ b/pkg/util/pki/generate.go @@ -4,12 +4,32 @@ "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +const ( + // MinRSAKeySize is the minimum RSA keysize allowed to be generated by the + // generator functions in this package. + MinRSAKeySize = 2048 + + // MaxRSAKeySize is the maximum RSA keysize allowed to be generated by the + // generator functions in this package. + MaxRSAKeySize = 8192 + + // ECCurve256 represents a secp256r1 / prime256v1 / NIST P-256 ECDSA key. + ECCurve256 = 256 + // ECCurve384 represents a secp384r1 / NIST P-384 ECDSA key. + ECCurve384 = 384 + // ECCurve521 represents a secp521r1 / NIST P-521 ECDSA key. + ECCurve521 = 521 ) // EncodePrivateKey will encode a given crypto.PrivateKey by first inspecting @@ -63,3 +83,119 @@ block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes} return pem.EncodeToMemory(block), nil } + +// PublicKeyMatchesCSR can be used to verify the given public key matches the +// public key in the given x509.CertificateRequest. +// Returns false and no error if the given public key is *not* the same as the CSR's key +// Returns true and no error if the given public key *is* the same as the CSR's key +// Returns an error if the CSR's key type cannot be determined (i.e. non RSA/ECDSA keys) +func PublicKeyMatchesCSR(check crypto.PublicKey, csr *x509.CertificateRequest) (bool, error) { + return PublicKeysEqual(csr.PublicKey, check) +} + +// PublicKeysEqual compares two given public keys for equality. +// The definition of "equality" depends on the type of the public keys. +// Returns true if the keys are the same, false if they differ or an error if +// the key type of `a` cannot be determined. +func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) { + switch pub := a.(type) { + case *rsa.PublicKey: + return pub.Equal(b), nil + case *ecdsa.PublicKey: + return pub.Equal(b), nil + case ed25519.PublicKey: + return pub.Equal(b), nil + default: + return false, fmt.Errorf("unrecognised public key type: %T", a) + } +} + +// GeneratePrivateKeyForCertificate will generate a private key suitable for +// the provided cert-manager Certificate resource, taking into account the +// parameters on the provided resource. +// The returned key will either be RSA or ECDSA. +func GeneratePrivateKeyForCertificate(crt *v1.Certificate) (crypto.Signer, error) { + crt = crt.DeepCopy() + if crt.Spec.PrivateKey == nil { + crt.Spec.PrivateKey = &v1.CertificatePrivateKey{} + } + switch crt.Spec.PrivateKey.Algorithm { + case v1.PrivateKeyAlgorithm(""), v1.RSAKeyAlgorithm: + keySize := MinRSAKeySize + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateRSAPrivateKey(keySize) + case v1.ECDSAKeyAlgorithm: + keySize := ECCurve256 + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateECPrivateKey(keySize) + case v1.Ed25519KeyAlgorithm: + return GenerateEd25519PrivateKey() + default: + return nil, fmt.Errorf("unsupported private key algorithm specified: %s", crt.Spec.PrivateKey.Algorithm) + } +} + +// GenerateRSAPrivateKey will generate a RSA private key of the given size. +// It places restrictions on the minimum and maximum RSA keysize. +func GenerateRSAPrivateKey(keySize int) (*rsa.PrivateKey, error) { + // Do not allow keySize < 2048 + // https://en.wikipedia.org/wiki/Key_size#cite_note-twirl-14 + if keySize < MinRSAKeySize { + return nil, fmt.Errorf("weak rsa key size specified: %d. minimum key size: %d", keySize, MinRSAKeySize) + } + if keySize > MaxRSAKeySize { + return nil, fmt.Errorf("rsa key size specified too big: %d. maximum key size: %d", keySize, MaxRSAKeySize) + } + + return rsa.GenerateKey(rand.Reader, keySize) +} + +// GenerateECPrivateKey will generate an ECDSA private key of the given size. +// It can be used to generate 256, 384 and 521 sized keys. +func GenerateECPrivateKey(keySize int) (*ecdsa.PrivateKey, error) { + var ecCurve elliptic.Curve + + switch keySize { + case ECCurve256: + ecCurve = elliptic.P256() + case ECCurve384: + ecCurve = elliptic.P384() + case ECCurve521: + ecCurve = elliptic.P521() + default: + return nil, fmt.Errorf("unsupported ecdsa key size specified: %d", keySize) + } + + return ecdsa.GenerateKey(ecCurve, rand.Reader) +} + +// GenerateEd25519PrivateKey will generate an Ed25519 private key +func GenerateEd25519PrivateKey() (ed25519.PrivateKey, error) { + + _, prvkey, err := ed25519.GenerateKey(rand.Reader) + + return prvkey, err +} + +// PublicKeyForPrivateKey will return the crypto.PublicKey for the given +// crypto.PrivateKey. It only supports RSA and ECDSA keys. +func PublicKeyForPrivateKey(pk crypto.PrivateKey) (crypto.PublicKey, error) { + switch k := pk.(type) { + case *rsa.PrivateKey: + return k.Public(), nil + case *ecdsa.PrivateKey: + return k.Public(), nil + case ed25519.PrivateKey: + return k.Public(), nil + default: + return nil, fmt.Errorf("unknown private key type: %T", pk) + } +} diff --git a/pkg/util/pki/keyusage.go b/pkg/util/pki/keyusage.go new file mode 100644 index 0000000..6404b4d --- /dev/null +++ b/pkg/util/pki/keyusage.go @@ -0,0 +1,135 @@ +package pki + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" +) + +// Copied from x509.go +var ( + OIDExtensionKeyUsage = []int{2, 5, 29, 15} + OIDExtensionExtendedKeyUsage = []int{2, 5, 29, 37} + OIDExtensionBasicConstraints = []int{2, 5, 29, 19} +) + +// RFC 5280, 4.2.1.12 Extended Key Usage +// +// anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 } +// +// id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } +// +// id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } +// id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } +// id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 } +// id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 } +// id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 } +// id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } +var ( + oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} + oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} + oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} + oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} + oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} + oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} + oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} + oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} + oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} + oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} + oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} + oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} + oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} + oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1} +) + +// extKeyUsageOIDs contains the mapping between an ExtKeyUsage and its OID. +var extKeyUsageOIDs = []struct { + extKeyUsage x509.ExtKeyUsage + oid asn1.ObjectIdentifier +}{ + {x509.ExtKeyUsageAny, oidExtKeyUsageAny}, + {x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth}, + {x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth}, + {x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning}, + {x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection}, + {x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem}, + {x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel}, + {x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser}, + {x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping}, + {x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning}, + {x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto}, + {x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto}, + {x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning}, + {x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning}, +} + +// OIDFromExtKeyUsage returns the ASN1 Identifier for a x509.ExtKeyUsage +func OIDFromExtKeyUsage(eku x509.ExtKeyUsage) (oid asn1.ObjectIdentifier, ok bool) { + for _, pair := range extKeyUsageOIDs { + if eku == pair.extKeyUsage { + return pair.oid, true + } + } + return +} + +func ExtKeyUsageFromOID(oid asn1.ObjectIdentifier) (eku x509.ExtKeyUsage, ok bool) { + for _, pair := range extKeyUsageOIDs { + if oid.Equal(pair.oid) { + return pair.extKeyUsage, true + } + } + return +} + +// asn1BitLength returns the bit-length of bitString by considering the +// most-significant bit in a byte to be the "first" bit. This convention +// matches ASN.1, but differs from almost everything else. +func asn1BitLength(bitString []byte) int { + bitLen := len(bitString) * 8 + + for i := range bitString { + b := bitString[len(bitString)-i-1] + + for bit := uint(0); bit < 8; bit++ { + if (b>>bit)&1 == 1 { + return bitLen + } + bitLen-- + } + } + + return 0 +} + +// Copied from x509.go +func reverseBitsInAByte(in byte) byte { + b1 := in>>4 | in<<4 + b2 := b1>>2&0x33 | b1<<2&0xcc + b3 := b2>>1&0x55 | b2<<1&0xaa + return b3 +} + +// Adapted from x509.go +func buildASN1KeyUsageRequest(usage x509.KeyUsage) (pkix.Extension, error) { + OIDExtensionKeyUsage := pkix.Extension{ + Id: OIDExtensionKeyUsage, + } + var a [2]byte + a[0] = reverseBitsInAByte(byte(usage)) + a[1] = reverseBitsInAByte(byte(usage >> 8)) + + l := 1 + if a[1] != 0 { + l = 2 + } + + bitString := a[:l] + var err error + OIDExtensionKeyUsage.Value, err = asn1.Marshal(asn1.BitString{Bytes: bitString, BitLength: asn1BitLength(bitString)}) + if err != nil { + return pkix.Extension{}, err + } + + return OIDExtensionKeyUsage, nil +} diff --git a/pkg/util/pki/parse.go b/pkg/util/pki/parse.go index e8ead01..3c7399d 100644 --- a/pkg/util/pki/parse.go +++ b/pkg/util/pki/parse.go @@ -3,8 +3,12 @@ import ( "crypto" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" + "github.com/go-ldap/ldap/v3" + errors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" ) @@ -99,3 +103,74 @@ } } + +var OIDConstants = struct { + Country []int + Organization []int + OrganizationalUnit []int + CommonName []int + SerialNumber []int + Locality []int + Province []int + StreetAddress []int +}{ + Country: []int{2, 5, 4, 6}, + Organization: []int{2, 5, 4, 10}, + OrganizationalUnit: []int{2, 5, 4, 11}, + CommonName: []int{2, 5, 4, 3}, + SerialNumber: []int{2, 5, 4, 5}, + Locality: []int{2, 5, 4, 7}, + Province: []int{2, 5, 4, 8}, + StreetAddress: []int{2, 5, 4, 9}, +} + +// Copied from pkix.attributeTypeNames and inverted. (Sadly it is private.) +// Source: https://cs.opensource.google/go/go/+/refs/tags/go1.18.2:src/crypto/x509/pkix/pkix.go;l=26 +var attributeTypeNames = map[string][]int{ + "C": OIDConstants.Country, + "O": OIDConstants.Organization, + "OU": OIDConstants.OrganizationalUnit, + "CN": OIDConstants.CommonName, + "SERIALNUMBER": OIDConstants.SerialNumber, + "L": OIDConstants.Locality, + "ST": OIDConstants.Province, + "STREET": OIDConstants.StreetAddress, +} + +func ParseSubjectStringToRdnSequence(subject string) (pkix.RDNSequence, error) { + + dns, err := ldap.ParseDN(subject) + if err != nil { + return nil, err + } + + // Traverse the parsed RDNSequence in REVERSE order as RDNs in String format are expected to be written in reverse order. + // Meaning, a string of "CN=Foo,OU=Bar,O=Baz" actually should have "O=Baz" as the first element in the RDNSequence. + var rdns pkix.RDNSequence + for i := range dns.RDNs { + ldapRelativeDN := dns.RDNs[len(dns.RDNs)-i-1] + + var atvs []pkix.AttributeTypeAndValue + for _, ldapATV := range ldapRelativeDN.Attributes { + + atvs = append(atvs, pkix.AttributeTypeAndValue{ + Type: attributeTypeNames[ldapATV.Type], + Value: ldapATV.Value, + }) + + } + rdns = append(rdns, atvs) + } + return rdns, nil + +} + +func ParseSubjectStringToRawDerBytes(subject string) ([]byte, error) { + rdnSequenceFromLiteralString, err := ParseSubjectStringToRdnSequence(subject) + if err != nil { + return nil, err + } + + return asn1.Marshal(rdnSequenceFromLiteralString) + +} diff --git a/Makefile b/Makefile index 3f72ea7..a8ac9ff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. ## skip the rbac:roleName=manager-role - $(CONTROLLER_GEN) crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd paths="./..." output:crd:artifacts:config=config/crd/bases MODULE_NAME ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager INPUT_APIS ?= gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1,gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1 diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 1780d5b..03402c0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -4,7 +4,10 @@ "fmt" "strings" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificaterequests/selfsigned" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/keymanager" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/requestmanager" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" @@ -37,12 +40,18 @@ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, trigger.ControllerName, + requestmanager.ControllerName, + keymanager.ControllerName, + selfsigned.CRControllerName, } ) diff --git a/config/crd/patches/webhook_in_certificates.yaml b/config/crd/patches/webhook_in_certificates.yaml deleted file mode 100644 index e24568f..0000000 --- a/config/crd/patches/webhook_in_certificates.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.anthos-cert-manager.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/rbac/event_editor_role.yaml b/config/rbac/event_editor_role.yaml new file mode 100644 index 0000000..b61e79b --- /dev/null +++ b/config/rbac/event_editor_role.yaml @@ -0,0 +1,21 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: event-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernets.io/managed-by: kustomize + name: event-editor-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch \ No newline at end of file diff --git a/config/rbac/event_editor_role_binding.yaml b/config/rbac/event_editor_role_binding.yaml new file mode 100644 index 0000000..a166aad --- /dev/null +++ b/config/rbac/event_editor_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: event-editor-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: anthoscertmanager + app.kubernetes.io/part-of: anthoscertmanager + app.kubernetes.io/managed-by: kustomize + name: event-editor-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: event-editor-role +subjects: +- kind: ServiceAccount + name: anthos-certificate-manager + namespace: anthoscertmanager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index ebff82f..f12374d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,5 @@ - secret_viewer_clusterrole_binding.yaml - issuers_clusterrole.yaml - issuers_clusterrolebinding.yaml +- event_editor_role.yaml +- event_editor_role_binding.yaml diff --git a/go.mod b/go.mod index 72f65c7..f021c47 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.18 require ( + github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-logr/logr v1.2.3 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.0 github.com/spf13/cobra v1.4.0 @@ -26,11 +27,13 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -50,7 +53,7 @@ github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index 140e6ab..0bc38a9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -97,9 +99,13 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -256,6 +262,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -277,8 +284,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 829ca62..c15dc77 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -156,3 +156,12 @@ } return nil } + +func GetCertificateRequestCondition(req *acmapi.CertificateRequest, conditionType acmapi.CertificateRequestConditionType) *acmapi.CertificateRequestCondition { + for _, cond := range req.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/api/util/duration.go b/pkg/api/util/duration.go new file mode 100644 index 0000000..b92aade --- /dev/null +++ b/pkg/api/util/duration.go @@ -0,0 +1,20 @@ +package util + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// DefaultCertDuration returns d.Duration if set, otherwise returns +// cert-manager's default certificate duration (90 days). +func DefaultCertDuration(d *metav1.Duration) time.Duration { + certDuration := v1.DefaultCertificateDuration + if d != nil { + certDuration = d.Duration + } + + return certDuration +} diff --git a/pkg/api/util/names.go b/pkg/api/util/names.go new file mode 100644 index 0000000..dc483f4 --- /dev/null +++ b/pkg/api/util/names.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + "regexp" +) + +// ComputeName hashes the given object and prefixes it with prefix. +// The algorithm in use is Fowler–Noll–Vo hash function and is not +// cryptographically secure. Using a cryptographically secure hash is +// not necessary. +func ComputeName(prefix string, obj interface{}) (string, error) { + objectBytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(objectBytes) + if err != nil { + return "", err + } + + // we're shortening to stay under 64 as we use this in services + // and pods down the road for ACME resources. + prefix = DNSSafeShortenTo52Characters(prefix) + + return fmt.Sprintf("%s-%d", prefix, hashF.Sum32()), nil +} + +// DNSSafeShortenTo52Characters shortens the input string to 52 chars and ensures the last char is an alpha-numeric character. +func DNSSafeShortenTo52Characters(in string) string { + if len(in) >= 52 { + validCharIndexes := regexp.MustCompile(`[a-zA-Z\d]`).FindAllStringIndex(fmt.Sprintf("%.52s", in), -1) + in = in[:validCharIndexes[len(validCharIndexes)-1][1]] + } + + return in +} diff --git a/pkg/api/util/usages.go b/pkg/api/util/usages.go new file mode 100644 index 0000000..4977741 --- /dev/null +++ b/pkg/api/util/usages.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/x509" + "math/bits" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +var keyUsages = map[acmapi.KeyUsage]x509.KeyUsage{ + acmapi.UsageSigning: x509.KeyUsageDigitalSignature, + acmapi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, + acmapi.UsageContentCommitment: x509.KeyUsageContentCommitment, + acmapi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, + acmapi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, + acmapi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, + acmapi.UsageCertSign: x509.KeyUsageCertSign, + acmapi.UsageCRLSign: x509.KeyUsageCRLSign, + acmapi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, + acmapi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, +} + +var extKeyUsages = map[acmapi.KeyUsage]x509.ExtKeyUsage{ + acmapi.UsageAny: x509.ExtKeyUsageAny, + acmapi.UsageServerAuth: x509.ExtKeyUsageServerAuth, + acmapi.UsageClientAuth: x509.ExtKeyUsageClientAuth, + acmapi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, + acmapi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, + acmapi.UsageSMIME: x509.ExtKeyUsageEmailProtection, + acmapi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, + acmapi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, + acmapi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, + acmapi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, + acmapi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, + acmapi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, + acmapi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, +} + +// KeyUsageType returns the relevant x509.KeyUsage or false if not found +func KeyUsageType(usage acmapi.KeyUsage) (x509.KeyUsage, bool) { + u, ok := keyUsages[usage] + return u, ok +} + +// ExtKeyUsageType returns the relevant x509.ExtKeyUsage or false if not found +func ExtKeyUsageType(usage acmapi.KeyUsage) (x509.ExtKeyUsage, bool) { + eu, ok := extKeyUsages[usage] + return eu, ok +} + +// KeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func KeyUsageStrings(usage x509.KeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for i := 0; i < bits.UintSize; i++ { + if v := usage & (1 << uint(i)); v != 0 { + usageStr = append(usageStr, keyUsageString(v)) + } + } + + return usageStr +} + +// ExtKeyUsageStrings returns the acmapi.KeyUsage and "unknown" if not found +func ExtKeyUsageStrings(usage []x509.ExtKeyUsage) []acmapi.KeyUsage { + var usageStr []acmapi.KeyUsage + + for _, u := range usage { + usageStr = append(usageStr, extKeyUsageString(u)) + } + + return usageStr +} + +// keyUsageString returns the acmapi.KeyUsage and "unknown" if not found +func keyUsageString(usage x509.KeyUsage) acmapi.KeyUsage { + for k, v := range keyUsages { + if usage == x509.KeyUsageDigitalSignature { + return acmapi.UsageDigitalSignature // we have KeyUsageDigitalSignature twice in our array, we should be consistent when parsing + } + if usage == v { + return k + } + } + + return "unknown" +} + +// extKeyUsageString returns the acmapi.ExtKeyUsage and "unknown" if not found +func extKeyUsageString(usage x509.ExtKeyUsage) acmapi.KeyUsage { + for k, v := range extKeyUsages { + if usage == v { + return k + } + } + + return "unknown" +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 80b0123..b189a55 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -102,11 +102,16 @@ // CertificateSpec defines the desired state of Certificate type CertificateSpec struct { - // Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). // +optional Subject *X509Subject `json:"subject,omitempty"` + // LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). + // Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. + // This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. + // +optional + LiteralSubject string `json:"literalSubject,omitempty"` + // CommonName is a common name to be used on the Certificate. // The CommonName should have a length of 64 characters or fewer to avoid // generating invalid CSRs. @@ -115,6 +120,15 @@ // +optional CommonName string `json:"commonName,omitempty"` + // The requested 'duration' (i.e. lifetime) of the Certificate. This option + // may be ignored/overridden by some issuer types. If unset this defaults to + // 90 days. Certificate will be renewed either 2/3 through its duration or + // `renewBefore` period before its expiry, whichever is later. Minimum + // accepted duration is 1 hour. Value must be in units accepted by Go + // time.ParseDuration https://golang.org/pkg/time/#ParseDuration + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + // How long before the currently issued certificate's expiry // cert-manager should renew the certificate. The default is 2/3 of the // issued certificate's duration. Minimum accepted value is 5 minutes. @@ -127,35 +141,17 @@ // +optional DNSNames []string `json:"dnsNames,omitempty"` - // The requested 'duration' (i.e. lifetime) of the Certificate. This option - // may be ignored/overridden by some issuer types. If unset this defaults to - // 90 days. Certificate will be renewed either 2/3 through its duration or - // `renewBefore` period before its expiry, whichever is later. Minimum - // accepted duration is 1 hour. Value must be in units accepted by Go - // time.ParseDuration https://golang.org/pkg/time/#ParseDuration - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` - // IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. // +optional IPAddresses []string `json:"ipAddresses,omitempty"` - // IsCA will mark this Certificate as valid for certificate signing. - // This will automatically add the `cert sign` usage to the list of `usages`. + // URIs is a list of URI subjectAltNames to be set on the Certificate. // +optional - IsCA bool `json:"isCA,omitempty"` + URIs []string `json:"uris,omitempty"` - // IssuerRef is a reference to the issuer for this certificate. - // If the `kind` field is not set, or set to `Issuer`, an Issuer resource - // with the given name in the same namespace as the Certificate will be used. - // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the - // provided name will be used. - // The `name` field in this stanza is required at all times. - IssuerRef acmmeta.ObjectReference `json:"issuerRef"` - - // Options to control private keys used for the Certificate. + // EmailAddresses is a list of email subjectAltNames to be set on the Certificate. // +optional - PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` // SecretName is the name of the secret resource that will be automatically // created and managed by this Certificate resource. @@ -175,6 +171,28 @@ // `secretName` Secret resource. // +optional Keystores *CertificateKeystores `json:"keystores,omitempty"` + + // IssuerRef is a reference to the issuer for this certificate. + // If the `kind` field is not set, or set to `Issuer`, an Issuer resource + // with the given name in the same namespace as the Certificate will be used. + // If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the + // provided name will be used. + // The `name` field in this stanza is required at all times. + IssuerRef acmmeta.ObjectReference `json:"issuerRef"` + + // IsCA will mark this Certificate as valid for certificate signing. + // This will automatically add the `cert sign` usage to the list of `usages`. + // +optional + IsCA bool `json:"isCA,omitempty"` + + // Usages is the set of x509 usages that are requested for the certificate. + // Defaults to `digital signature` and `key encipherment` if not specified. + // +optional + Usages []KeyUsage `json:"usages,omitempty"` + + // Options to control private keys used for the Certificate. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // CertificatePrivateKey contains configuration options for private keys @@ -222,10 +240,6 @@ Size int `json:"size,omitempty"` // Validated by webhook. Be mindful of adding OpenAPI validation- see https://github.com/cert-manager/cert-manager/issues/3644 } -// Denotes how private keys should be generated or sourced when a Certificate -// is being issued. -type PrivateKeyRotationPolicy string - // CertificateConditionType represents an Certificate condition value. type CertificateConditionType string @@ -384,6 +398,22 @@ Labels map[string]string `json:"labels,omitempty"` } +// Denotes how private keys should be generated or sourced when a Certificate +// is being issued. +type PrivateKeyRotationPolicy string + +var ( + // RotationPolicyNever means a private key will only be generated if one + // does not already exist in the target `spec.secretName`. + // If one does exists but it does not have the correct algorithm or size, + // a warning will be raised to await user intervention. + RotationPolicyNever PrivateKeyRotationPolicy = "Never" + + // RotationPolicyAlways means a private key matching the specified + // requirements will be generated whenever a re-issuance occurs. + RotationPolicyAlways PrivateKeyRotationPolicy = "Always" +) + // X509Subject Full X509 name specification type X509Subject struct { // Organizations to be used on the Certificate. diff --git a/pkg/apis/anthoscertmanager/v1/const.go b/pkg/apis/anthoscertmanager/v1/const.go new file mode 100644 index 0000000..5c403d8 --- /dev/null +++ b/pkg/apis/anthoscertmanager/v1/const.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Deprecated: the default is now 2/3 of Certificate's duration + DefaultRenewBefore = time.Hour * 24 * 30 +) + +const ( + // Default mount path location for Kubernetes ServiceAccount authentication + // (/v1/auth/kubernetes). The endpoint will then be called at `/login`, so + // left as the default, `/v1/auth/kubernetes/login` will be called. + DefaultVaultKubernetesAuthMountPath = "/v1/auth/kubernetes" +) diff --git a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go index 4448bce..ab47f1f 100644 --- a/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/anthoscertmanager/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ *out = new(X509Subject) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } if in.RenewBefore != nil { in, out := &in.RenewBefore, &out.RenewBefore *out = new(metav1.Duration) @@ -381,21 +386,20 @@ *out = make([]string, len(*in)) copy(*out, *in) } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(metav1.Duration) - **out = **in - } if in.IPAddresses != nil { in, out := &in.IPAddresses, &out.IPAddresses *out = make([]string, len(*in)) copy(*out, *in) } - out.IssuerRef = in.IssuerRef - if in.PrivateKey != nil { - in, out := &in.PrivateKey, &out.PrivateKey - *out = new(CertificatePrivateKey) - **out = **in + if in.URIs != nil { + in, out := &in.URIs, &out.URIs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailAddresses != nil { + in, out := &in.EmailAddresses, &out.EmailAddresses + *out = make([]string, len(*in)) + copy(*out, *in) } if in.SecretTemplate != nil { in, out := &in.SecretTemplate, &out.SecretTemplate @@ -407,6 +411,17 @@ *out = new(CertificateKeystores) (*in).DeepCopyInto(*out) } + out.IssuerRef = in.IssuerRef + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]KeyUsage, len(*in)) + copy(*out, *in) + } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. diff --git a/pkg/controller/certificaterequests/checks.go b/pkg/controller/certificaterequests/checks.go new file mode 100644 index 0000000..8962e6c --- /dev/null +++ b/pkg/controller/certificaterequests/checks.go @@ -0,0 +1,63 @@ +package certificaterequests + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "k8s.io/apimachinery/pkg/labels" +) + +func (c *controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(acmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificatesRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *controller) certificatesRequestsForGenericIssuer(iss acmapi.GenericIssuer) ([]*acmapi.CertificateRequest, error) { + crts, err := c.certificateRequestLister.List(labels.NewSelector()) + + if err != nil { + return nil, fmt.Errorf("error listing certificates: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*acmapi.ClusterIssuer) + + var affected []*acmapi.CertificateRequest + for _, crt := range crts { + if isClusterIssuer && crt.Spec.IssuerRef.Kind != acmapi.ClusterIssuerKind { + continue + } + if !isClusterIssuer { + if crt.Namespace != iss.GetObjectMeta().Namespace { + continue + } + } + if crt.Spec.IssuerRef.Name != iss.GetObjectMeta().Name { + continue + } + affected = append(affected, crt) + } + + return affected, nil +} diff --git a/pkg/controller/certificaterequests/controller.go b/pkg/controller/certificaterequests/controller.go new file mode 100644 index 0000000..e112ff2 --- /dev/null +++ b/pkg/controller/certificaterequests/controller.go @@ -0,0 +1,178 @@ +package certificaterequests + +import ( + "context" + "fmt" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +var keyFunc = controllerpkg.KeyFunc + +// Issuer implements the funcationalitiy to sign a certificate request for a particular issue type. +type Issuer interface { + Sign(context.Context, *v1.CertificateRequest, v1.GenericIssuer) (*issuer.IssueResponse, error) +} + +// Issuer Contractor builds a Issuer instance using the given controller +// context. +type IssuerConstructor func(*controllerpkg.Context) Issuer + +type controller struct { + //helper issuer.Helper + + // clientset used to update cert-manager API resources + acmClient acmclient.Interface + + // fieldManager is the manager name used for the Apply operations. + fieldManager string + + certificateRequestLister acmlisters.CertificateRequestLister + + // we need to wait for Secrets to be synced to avoid a situation where CA issuer's Secret + // is not yet in cached at a time when issuance is attempted, + // more details at https://github.com/cert-manager/cert-manager/issues/5216 + secretLister corelisters.SecretLister + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // the issuer kind to react to when a certificate request is synced + issuerType string + + issuerLister acmlisters.IssuerLister + clusterIssuerLister acmlisters.ClusterIssuerLister + + // extraInformerResources are the set of resources which should cause + // reconciles if owned by a CertifcateRequest. + extraInformerResources []schema.GroupVersionResource + + // Issuer to call sign function + issuerConstructor IssuerConstructor + issuer Issuer + + // used for testing + clock clock.Clock + + // reporter *util.Reporter +} + +// NewController will construct a new certificaterequest controller using the given +// Issuer implementation. +func NewController(issuerType string, issuerConstructor IssuerConstructor, extraInformerResources ...schema.GroupVersionResource) *controller { + return &controller{ + issuerType: issuerType, + issuerConstructor: issuerConstructor, + extraInformerResources: extraInformerResources, + } +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + componentName := "certificaterequests-issuer-" + c.issuerType + + c.log = logf.FromContext(ctx.RootContext, componentName) + + // create a working queue + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), componentName) + + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + issuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().Issuers() + c.issuerLister = issuerInformer.Lister() + c.secretLister = secretsInformer.Lister() + + // obtain references to all the informers used by this controller + certificateRequestInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().CertificateRequests() + + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + } + + // If the manger is scoped to all namespaces, we should also obtain a lister for clusterissuers. + if ctx.Namespace == "" { + clusterIssuerInformer := ctx.SharedInformerFactory.AnthosCertmanager().V1().ClusterIssuers() + c.clusterIssuerLister = clusterIssuerInformer.Lister() + + // register handler function for cluster issuers resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + } + + c.certificateRequestLister = certificateRequestInformer.Lister() + + // register handler functions + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + // c.helper = issuer.NewHelper(c.issuerLister, c.clusterIssuerLister) + + // clock is used to set the FailureTime of failed CertificateRequests + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + // c.reporter = util.NewReporter(c.clock, c.recorder) + c.acmClient = ctx.ACMClient + c.fieldManager = ctx.FieldManager + + // Construct the issuer implementation with the built component context. + c.issuer = c.issuerConstructor(ctx) + + c.log.V(logf.DebugLevel).Info("new certificate request controller registered", + "type", c.issuerType) + + return c.queue, mustSync, nil + +} + +// ProcessItem is the worker function that will be called with a new key from +// the workqueue. A key corresponds to a certificate request object. +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if err != nil { + if k8sErrors.IsNotFound(err) { + dbg.Info(fmt.Sprintf("certificate request in work queue no longer exists: %s", err)) + return nil + } + + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, cr)) + return c.Sync(ctx, cr) +} + +func certificateRequestGetter(lister acmlisters.CertificateRequestLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.CertificateRequests(namespace).Get(name) + } +} diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned.go b/pkg/controller/certificaterequests/selfsigned/selfsigned.go new file mode 100644 index 0000000..78b9eb6 --- /dev/null +++ b/pkg/controller/certificaterequests/selfsigned/selfsigned.go @@ -0,0 +1,143 @@ +package selfsigned + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + acmerrors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/kube" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/record" +) + +const ( + CRControllerName = "certificaterequests-issuer-selfsigned" + emptyDNMessage = "Certificate will be issued with an empty Issuer DN, which contravenes RFC 5280 and could break some strict clients" +) + +type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error) + +type SelfSigned struct { + issuerOptions controllerpkg.IssuerOptions + secretsLister corelisters.SecretLister + + // reporter *crutil.Reporter + recorder record.EventRecorder + + // Used for testing to get reproducible resulting certificates + signingFn signingFn +} + +func (s *SelfSigned) Sign(ctx context.Context, cr *acmapi.CertificateRequest, issuerObj acmapi.GenericIssuer) (*issuer.IssueResponse, error) { + log := logf.FromContext(ctx, "sign") + resourceNamespace := s.issuerOptions.ResourceNamespace(issuerObj) + + secretName, ok := cr.ObjectMeta.Annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] + if !ok || secretName == "" { + message := fmt.Sprintf("Annotation %q missing or reference empty", acmapi.CertificateRequestPrivateKeyAnnotationKey) + err := errors.New("secret name missing") + // s.reporter.Failed(cr, err, "MissingAnnotation", message) + log.Error(err, message) + return nil, nil + } + + privatekey, err := kube.SecretTLSKey(ctx, s.secretsLister, cr.Namespace, secretName) + if k8sErrors.IsNotFound(err) { + message := fmt.Sprintf("Referenced secret %s/%s not found", cr.Namespace, secretName) + + //s.reporter.Pending(cr, err, "MissingSecret", message) + log.Error(err, message) + + return nil, nil + } + + if acmerrors.IsInvalidData(err) { + message := fmt.Sprintf("Failed to get key %q referenced in annotation %q", + secretName, acmapi.CertificateRequestPrivateKeyAnnotationKey) + + //s.reporter.Pending(cr, err, "ErrorParsingKey", message) + log.Error(err, message) + + return nil, nil + } + + if err != nil { + // We are probably in a network error here so we should backoff and retry + message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName) + //s.reporter.Pending(cr, err, "ErrorGettingSecret", message) + log.Error(err, message) + return nil, err + } + + template, err := pki.GenerateTemplateFromCertificateRequest(cr) + if err != nil { + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorGenerating", message) + log.Error(err, message) + return nil, nil + } + + template.CRLDistributionPoints = issuerObj.GetSpec().SelfSigned.CRLDistributionPoints + + if template.Subject.String() == "" { + // RFC 5280 (https://tools.ietf.org/html/rfc5280#section-4.1.2.4) says that: + // "The issuer field MUST contain a non-empty distinguished name (DN)." + // Since we're creating a self-signed cert, the issuer will match whatever is + // in the template's subject DN. + log.V(logf.DebugLevel).Info("issued cert will have an empty issuer DN, which contravenes RFC 5280. emitting warning event") + s.recorder.Event(cr, corev1.EventTypeWarning, "BadConfig", emptyDNMessage) + } + + // extract the public component of the key + publickey, err := pki.PublicKeyForPrivateKey(privatekey) + if err != nil { + message := "Failed to get public key from private key" + //s.reporter.Failed(cr, err, "ErrorPublicKey", message) + log.Error(err, message) + return nil, nil + } + + ok, err = pki.PublicKeysEqual(publickey, template.PublicKey) + if err != nil || !ok { + + if err == nil { + err = errors.New("CSR not signed by referenced private key") + } + + message := "Error generating certificate template" + //s.reporter.Failed(cr, err, "ErrorKeyMatch", message) + log.Error(err, message) + + return nil, nil + } + + // sign and encode the certificate + certPem, _, err := s.signingFn(template, template, publickey, privatekey) + if err != nil { + message := "Error signing certificate" + //s.reporter.Failed(cr, err, "ErrorSigning", message) + log.Error(err, message) + return nil, nil + } + + log.V(logf.DebugLevel).Info("self signed certificate issued") + + // We set the CA to the returned certificate here since this is self signed. + return &issuer.IssueResponse{ + Certificate: certPem, + CA: certPem, + }, nil + +} diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go new file mode 100644 index 0000000..5edf6fb --- /dev/null +++ b/pkg/controller/certificaterequests/sync.go @@ -0,0 +1,11 @@ +package certificaterequests + +import ( + "context" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func (c *controller) Sync(ctx context.Context, cr *acmapi.CertificateRequest) (err error) { + return nil +} diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 1c89bc7..f28bc21 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -121,7 +121,7 @@ namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - return nil + return err } crt, err := c.certificateLister.Certificates(namespace).Get(name) @@ -185,7 +185,7 @@ // TODO: Once we move to only server-side apply API calls, this should be changed to setting the Issuing condition to False apiutil.RemoveCertificateCondition(crt, acmapi.CertificateConditionIssuing) - // Clearn the failed attempts + // Clean the failed attempts crt.Status.FailedIssuanceAttempts = nil // Clean status.lastFailureTime diff --git a/pkg/controller/certificates/keymanager/keymanager_controller.go b/pkg/controller/certificates/keymanager/keymanager_controller.go new file mode 100644 index 0000000..16c8e8b --- /dev/null +++ b/pkg/controller/certificates/keymanager/keymanager_controller.go @@ -0,0 +1,375 @@ +package keymanager + +import ( + "context" + "crypto" + "fmt" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/selection" + + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + ControllerName = "certificates-key-manager" + reasonDecodeFailed = "DecodeFailed" + reasonCannotRegenerateKey = "CannotRegenerateKey" + reasonDeleted = "Deleted" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + secretLister corelisters.SecretLister + client acmclient.Interface + coreClient kubernetes.Interface + recorder record.EventRecorder + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + coreClient kubernetes.Interface, + factory informers.SharedInformerFactory, + cmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := cmFactory.AnthosCertmanager().V1().Certificates() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to certificates named as spec.secretName + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName), + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + coreClient: coreClient, + recorder: recorder, + fieldManager: fieldManager, + }, queue, mustSync +} + +// isNextPrivateKeyLabelSelector is a label selector used to match Secret +// resources with the `cert-manager.io/next-private-key: "true"` label. +var isNextPrivateKeyLabelSelector labels.Selector + +func init() { + r, err := labels.NewRequirement("cert-manager.io/next-private-key", selection.Equals, []string{"true"}) + if err != nil { + panic(err) + } + isNextPrivateKeyLabelSelector = labels.NewSelector().Add(*r) +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Discover all 'owned' secrets that have the `next-private-key` label + secrets, err := certificates.ListSecretsMatchingPredicates(c.secretLister.Secrets(crt.Namespace), isNextPrivateKeyLabelSelector, predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + log.V(logf.DebugLevel).Info("Cleaning up Secret resources and unsetting nextPrivateKeySecretName as issuance is no longer in progress") + if err := c.deleteSecretResources(ctx, secrets); err != nil { + return err + } + return c.setNextPrivateKeySecretName(ctx, crt, nil) + } + + // if there is no existing Secret resource, create a new one + if len(secrets) == 0 { + rotationPolicy := acmapi.RotationPolicyNever + if crt.Spec.PrivateKey != nil && crt.Spec.PrivateKey.RotationPolicy != "" { + rotationPolicy = crt.Spec.PrivateKey.RotationPolicy + } + switch rotationPolicy { + case acmapi.RotationPolicyNever: + return c.createNextPrivateKeyRotationPolicyNever(ctx, crt) + case acmapi.RotationPolicyAlways: + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found") + return c.createAndSetNextPrivateKey(ctx, crt) + default: + log.V(logf.WarnLevel).Info("Certificate with unknown certificate.spec.privateKey.rotationPolicy value", "rotation_policy", rotationPolicy) + return nil + } + } + + // always clean up if multiple are found + if len(secrets) > 1 { + // TODO: if nextPrivateKeySecretName is set, we should skip deleting that one Secret resource + log.V(logf.DebugLevel).Info("Cleaning up Secret resources as multiple nextPrivateKeySecretName candidates found") + return c.deleteSecretResources(ctx, secrets) + } + + secret := secrets[0] + log = logf.WithRelatedResource(log, secret) + ctx = logf.NewContext(ctx, log) + + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("Adopting existing private key Secret") + return c.setNextPrivateKeySecretName(ctx, crt, &secret.Name) + } + if *crt.Status.NextPrivateKeySecretName != secrets[0].Name { + log.V(logf.DebugLevel).Info("Deleting existing private key secret as name does not match status.nextPrivateKeySecretName") + return c.deleteSecretResources(ctx, secrets) + } + + if secret.Data == nil || len(secret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Deleting Secret resource as it contains no data") + return c.deleteSecretResources(ctx, secrets) + } + pkData := secret.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(pkData) + if err != nil { + log.Error(err, "Deleting existing private key secret due to error decoding data") + return c.deleteSecretResources(ctx, secrets) + } + + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + log.Error(err, "Internal error verifying if private key matches spec - please open an issue.") + return nil + } + if len(violations) > 0 { + log.V(logf.DebugLevel).Info("Regenerating private key due to change in fields", "violations", violations) + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonDeleted, "Regenerating private key due to change in fields: %v", violations) + return c.deleteSecretResources(ctx, secrets) + } + + return nil +} + +func (c *controller) createNextPrivateKeyRotationPolicyNever(ctx context.Context, crt *acmapi.Certificate) error { + log := logf.FromContext(ctx) + s, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because no existing Secret found and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + if err != nil { + return err + } + if s.Data == nil || len(s.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Creating new nextPrivateKeySecretName Secret because existing Secret contains empty data and rotation policy is Never") + return c.createAndSetNextPrivateKey(ctx, crt) + } + existingPKData := s.Data[corev1.TLSPrivateKeyKey] + pk, err := pki.DecodePrivateKeyBytes(existingPKData) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to decode private key stored in Secret %q - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + violations, err := certificates.PrivateKeyMatchesSpec(pk, crt.Spec) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonDecodeFailed, "Failed to check if private key stored in Secret %q is up to date - generating new key", crt.Spec.SecretName) + return c.createAndSetNextPrivateKey(ctx, crt) + } + if len(violations) > 0 { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonCannotRegenerateKey, "User intervention required: existing private key in Secret %q does not match requirements on Certificate resource, mismatching fields: %v, but cert-manager cannot create new private key as the Certificate's .spec.privateKey.rotationPolicy is unset or set to Never. To allow cert-manager to create a new private key you can set .spec.privateKey.rotationPolicy to 'Always' (this will result in the private key being regenerated every time a cert is renewed) ", crt.Spec.SecretName, violations) + return nil + } + + nextPkSecret, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Reused", fmt.Sprintf("Reusing private key stored in existing Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &nextPkSecret.Name) +} + +func (c *controller) createAndSetNextPrivateKey(ctx context.Context, crt *acmapi.Certificate) error { + pk, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return err + } + + s, err := c.createNewPrivateKeySecret(ctx, crt, pk) + if err != nil { + return err + } + + c.recorder.Event(crt, corev1.EventTypeNormal, "Generated", fmt.Sprintf("Stored new private key in temporary Secret resource %q", s.Name)) + + return c.setNextPrivateKeySecretName(ctx, crt, &s.Name) +} + +// deleteSecretResources will delete the given secret resources +func (c *controller) deleteSecretResources(ctx context.Context, secrets []*corev1.Secret) error { + log := logf.FromContext(ctx) + for _, s := range secrets { + if err := c.coreClient.CoreV1().Secrets(s.Namespace).Delete(ctx, s.Name, metav1.DeleteOptions{}); err != nil { + return err + } + logf.WithRelatedResource(log, s).V(logf.DebugLevel).Info("Deleted 'next private key' Secret resource") + } + return nil +} + +func (c *controller) setNextPrivateKeySecretName(ctx context.Context, crt *acmapi.Certificate, name *string) error { + // skip updates if there has been no change + if name == nil && crt.Status.NextPrivateKeySecretName == nil { + return nil + } + if name != nil && crt.Status.NextPrivateKeySecretName != nil { + if *name == *crt.Status.NextPrivateKeySecretName { + return nil + } + } + crt = crt.DeepCopy() + crt.Status.NextPrivateKeySecretName = name + return c.updateOrApplyStatus(ctx, crt) +} + +// updateOrApplyStatus will update the controller status. +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err + +} + +func (c *controller) createNewPrivateKeySecret(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer) (*corev1.Secret, error) { + // if the 'nextPrivateKeySecretName' field is already set, use this as the + // name of the Secret resource. + name := "" + if crt.Status.NextPrivateKeySecretName != nil { + name = *crt.Status.NextPrivateKeySecretName + } + + pkData, err := pki.EncodePrivateKey(pk, acmapi.PKCS8) + if err != nil { + return nil, err + } + + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + Name: name, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + Labels: map[string]string{ + "cert-manager.io/next-private-key": "true", + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: pkData, + }, + } + if s.Name == "" { + // TODO: handle certificate resources that have especially long names + s.GenerateName = crt.Name + "-" + } + s, err = c.coreClient.CoreV1().Secrets(s.Namespace).Create(ctx, s, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return s, nil +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.Client, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go new file mode 100644 index 0000000..9fdaa1f --- /dev/null +++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go @@ -0,0 +1,436 @@ +package requestmanager + +import ( + "bytes" + "context" + "crypto" + "encoding/pem" + "fmt" + "strconv" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" +) + +const ( + ControllerName = "certificates-request-manager" + reasonRequestFailed = "RequestFailed" + reasonRequested = "Requested" +) + +var ( + certificateGvk = acmapi.SchemeGroupVersion.WithKind("Certificate") +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + + client acmclient.Interface + recorder record.EventRecorder + clock clock.Clock + fieldManager string +} + +func NewController( + log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + certificateControllerOptions controllerpkg.CertificateOptions, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' CertificateRequest resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + // Trigger reconciles on changes to any 'owned' secret resources + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ResourceOwnerOf, + ), + }) + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + secretsInformer.Informer().HasSynced, + certificateRequestInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + clock: clock, + // copiedAnnotationPrefixes: certificateControllerOptions.CopiedAnnotationPrefixes, + fieldManager: fieldManager, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + // Confirm the certificate has the issuing condition + if !apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + return nil + } + + // Check for and fetch the `status.nextPrivateKeySecretName` secret + if crt.Status.NextPrivateKeySecretName == nil { + log.V(logf.DebugLevel).Info("status.nextPrivateKeySecretName not yet set, waiting for keymanager before processing certificate") + return nil + } + nextPrivateKeySecret, err := c.secretLister.Secrets(crt.Namespace).Get(*crt.Status.NextPrivateKeySecretName) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("nextPrivateKeySecretName Secret resource does not exist, waiting for keymanager to create it before continuing") + return nil + } + if err != nil { + return err + } + if nextPrivateKeySecret.Data == nil || len(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) == 0 { + log.V(logf.DebugLevel).Info("Next private key secret does not contain any valid data, waiting for keymanager before processing certificate") + return nil + } + pk, err := pki.DecodePrivateKeyBytes(nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + log.Error(err, "Failed to decode next private key secret data, waiting for keymanager before processing certificate") + return nil + } + + // Discover all 'owned' CertificateRequests + requests, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), predicate.ResourceOwnedBy(crt)) + if err != nil { + return err + } + + // delete any existing CertificateRequest resources that do not have a + // revision annotation + if requests, err = c.deleteRequestsWithoutRevision(ctx, requests...); err != nil { + return err + } + + currentCertificateRevision := 0 + if crt.Status.Revision != nil { + currentCertificateRevision = *crt.Status.Revision + } + + nextRevision := currentCertificateRevision + 1 + + requests, err = requestsWithRevision(requests, currentCertificateRevision) + if err != nil { + return err + } + + requests, err = c.deleteRequestsNotMatchingSpec(ctx, crt, pk.Public(), requests...) + if err != nil { + return err + } + + requests, err = c.deleteCurrentFailedRequests(ctx, crt, requests...) + if err != nil { + return err + } + + if len(requests) > 1 { + log.V(logf.ErrorLevel).Info("Multiple matching CertificateRequest resources exist, delete one of them. This is likely an error and should be reported on the issue tracker!") + return nil + } + + if len(requests) == 1 { + // Nothing to do as we've already verified that the CertificateRequest + // is up to date above. + return nil + } + + return c.createNewCertificateRequest(ctx, crt, pk, nextRevision, nextPrivateKeySecret.Name) +} + +func requestsWithRevision(reqs []*acmapi.CertificateRequest, revision int) ([]*acmapi.CertificateRequest, error) { + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + return nil, fmt.Errorf("certificaterequest %q does not contain revision annotation", req.Name) + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + reqRevision, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + return nil, err + } + + if reqRevision == int64(revision) { + remaining = append(remaining, req) + } + } + return remaining, nil +} + +func (c *controller) deleteCurrentFailedRequests(ctx context.Context, crt *acmapi.Certificate, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx).WithValues("Certificate", crt.Name) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log = logf.WithRelatedResource(log, req) + + // Check if there are any 'current' CertificateRequests that + // failed during the previous issuance cycle. Those should be + // deleted so that a new one gets created and the issuance is + // re-tried. In practice no more than one CertificateRequest is + // expected at this point. + crReadyCond := apiutil.GetCertificateRequestCondition(req, acmapi.CertificateRequestConditionReady) + if crReadyCond == nil || crReadyCond.Status != acmmeta.ConditionFalse || crReadyCond.Reason != acmapi.CertificateRequestReasonFailed { + remaining = append(remaining, req) + continue + } + + certIssuingCond := apiutil.GetCertificateCondition(crt, acmapi.CertificateConditionIssuing) + if certIssuingCond == nil { + // This should never happen + log.V(logf.ErrorLevel).Info("Certificate does not have Issuing condition") + return nil, nil + } + // If the Issuing condition on the Certificate is newer than the + // failure time on CertificateRequest, it means that the + // CertificateRequest failed during the previous issuance (for the + // same revision). If it is a CertificateRequest that failed + // during the previous issuance, then it should be deleted so + // that we create a new one for this issuance. + if req.Status.FailureTime.Before(certIssuingCond.LastTransitionTime) { + log.V(logf.DebugLevel).Info("Found a failed CertificateRequest for previous issuance of this revision, deleting...") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsNotMatchingSpec(ctx context.Context, crt *acmapi.Certificate, publicKey crypto.PublicKey, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + violations, err := certificates.RequestMatchesSpec(req, crt.Spec) + if err != nil { + log.Error(err, "Failed to check if CertificateRequest matches spec, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + if len(violations) > 0 { + log.V(logf.InfoLevel).WithValues("violations", violations).Info("CertificateRequest does not match requirements on certificate.spec, deleting CertificateRequest", "violations", violations) + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + x509Req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + // this case cannot happen as RequestMatchesSpec would have returned an error too + return nil, err + } + matches, err := pki.PublicKeyMatchesCSR(publicKey, x509Req) + if err != nil { + return nil, err + } + if !matches { + log.V(logf.DebugLevel).Info("CertificateRequest contains a CSR that does not have the same public key as the stored next private key secret, deleting CertificateRequest") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) deleteRequestsWithoutRevision(ctx context.Context, reqs ...*acmapi.CertificateRequest) ([]*acmapi.CertificateRequest, error) { + log := logf.FromContext(ctx) + var remaining []*acmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Annotations == nil || req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == "" { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it does not contain a revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + reqRevisionStr := req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] + _, err := strconv.ParseInt(reqRevisionStr, 10, 0) + if err != nil { + log.V(logf.DebugLevel).Info("Deleting CertificateRequest as it contains an invalid revision annotation") + if err := c.client.AnthosCertmanagerV1().CertificateRequests(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}); err != nil { + return nil, err + } + continue + } + + remaining = append(remaining, req) + } + return remaining, nil +} + +func (c *controller) createNewCertificateRequest(ctx context.Context, crt *acmapi.Certificate, pk crypto.Signer, nextRevision int, nextPrivateKeySecretName string) error { + log := logf.FromContext(ctx) + x509CSR, err := pki.GenerateCSR(crt) + if err != nil { + log.Error(err, "Failed to generate CSR - will not retry") + return nil + } + csrDER, err := pki.EncodeCSR(x509CSR, pk) + if err != nil { + return err + } + + csrPEM := bytes.NewBuffer([]byte{}) + err = pem.Encode(csrPEM, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + if err != nil { + return err + } + + annotations := controllerpkg.BuildAnnotationsToCopy(crt.Annotations, []string{}) + annotations[acmapi.CertificateRequestRevisionAnnotationKey] = strconv.Itoa(nextRevision) + annotations[acmapi.CertificateRequestPrivateKeyAnnotationKey] = nextPrivateKeySecretName + annotations[acmapi.CertificateNameKey] = crt.Name + + cr := &acmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: crt.Namespace, + GenerateName: apiutil.DNSSafeShortenTo52Characters(crt.Name) + "-", + Annotations: annotations, + Labels: crt.Labels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: acmapi.CertificateRequestSpec{ + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + Request: csrPEM.Bytes(), + IsCA: crt.Spec.IsCA, + Usages: crt.Spec.Usages, + }, + } + + cr, err = c.client.AnthosCertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{FieldManager: c.fieldManager}) + if err != nil { + c.recorder.Eventf(crt, corev1.EventTypeWarning, reasonRequestFailed, "Failed to create CertificateRequest: "+err.Error()) + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, reasonRequested, "Created new CertificateRequest resource %q", cr.Name) + if err := c.waitForCertificateRequestToExist(cr.Namespace, cr.Name); err != nil { + return fmt.Errorf("failed whilst waiting for CertificateRequest to exist - this may indicate an apiserver running slowly. Request will be retried") + } + return nil +} + +func (c *controller) waitForCertificateRequestToExist(namespace, name string) error { + return wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) { + _, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name) + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + ctx.CertificateOptions, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go index 16b1b26..e4e7a12 100644 --- a/pkg/controller/certificates/utils.go +++ b/pkg/controller/certificates/utils.go @@ -1,8 +1,19 @@ package certificates import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "reflect" "time" + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +54,167 @@ rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) return &rt } + +// PrivateKeyMatchesSpec returns an error if the private key bit size +// doesn't match the provided spec. RSA, Ed25519 and ECDSA are supported. +// If any error is returned, a list of violations will also be returned. +func PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + spec = *spec.DeepCopy() + if spec.PrivateKey == nil { + spec.PrivateKey = &acmapi.CertificatePrivateKey{} + } + switch spec.PrivateKey.Algorithm { + case "", acmapi.RSAKeyAlgorithm: + return rsaPrivateKeyMatchesSpec(pk, spec) + case acmapi.Ed25519KeyAlgorithm: + return ed25519PrivateKeyMatchesSpec(pk, spec) + case acmapi.ECDSAKeyAlgorithm: + return ecdsaPrivateKeyMatchesSpec(pk, spec) + default: + return nil, fmt.Errorf("unrecognised key algorithm type %q", spec.PrivateKey.Algorithm) + } +} + +func rsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + rsaPk, ok := pk.(*rsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default RSA keySize is set to 2048. + keySize := pki.MinRSAKeySize + if spec.PrivateKey.Size > 0 { + keySize = spec.PrivateKey.Size + } + if rsaPk.N.BitLen() != keySize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ecdsaPrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + ecdsaPk, ok := pk.(*ecdsa.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + var violations []string + // TODO: we should not use implicit defaulting here, and instead rely on + // defaulting performed within the Kubernetes apiserver here. + // This requires careful handling in order to not interrupt users upgrading + // from older versions. + // The default EC curve type is EC256 + expectedKeySize := pki.ECCurve256 + if spec.PrivateKey.Size > 0 { + expectedKeySize = spec.PrivateKey.Size + } + if expectedKeySize != ecdsaPk.Curve.Params().BitSize { + violations = append(violations, "spec.privateKey.size") + } + return violations, nil +} + +func ed25519PrivateKeyMatchesSpec(pk crypto.PrivateKey, spec acmapi.CertificateSpec) ([]string, error) { + _, ok := pk.(ed25519.PrivateKey) + if !ok { + return []string{"spec.privateKey.algorithm"}, nil + } + + return nil, nil +} + +// RequestMatchesSpec compares a CertificateRequest with a CertificateSpec +// and returns a list of field names on the Certificate that do not match their +// counterpart fields on the CertificateRequest. +// If decoding the x509 certificate request fails, an error will be returned. +func RequestMatchesSpec(req *acmapi.CertificateRequest, spec acmapi.CertificateSpec) ([]string, error) { + x509req, err := pki.DecodeX509CertificateRequestBytes(req.Spec.Request) + if err != nil { + return nil, err + } + + // It is safe to mutate top-level fields in `spec` as it is not a pointer + // meaning changes will not effect the caller. + if spec.Subject == nil { + spec.Subject = &acmapi.X509Subject{} + } + + var violations []string + if spec.LiteralSubject == "" { + if x509req.Subject.CommonName != spec.CommonName { + violations = append(violations, "spec.commonName") + } + if !util.EqualUnsorted(x509req.DNSNames, spec.DNSNames) { + violations = append(violations, "spec.dnsNames") + } + if !util.EqualUnsorted(pki.IPAddressesToString(x509req.IPAddresses), spec.IPAddresses) { + violations = append(violations, "spec.ipAddresses") + } + if !util.EqualUnsorted(pki.URLsToString(x509req.URIs), spec.URIs) { + violations = append(violations, "spec.uris") + } + if !util.EqualUnsorted(x509req.EmailAddresses, spec.EmailAddresses) { + violations = append(violations, "spec.emailAddresses") + } + if x509req.Subject.SerialNumber != spec.Subject.SerialNumber { + violations = append(violations, "spec.subject.serialNumber") + } + if !util.EqualUnsorted(x509req.Subject.Organization, spec.Subject.Organizations) { + violations = append(violations, "spec.subject.organizations") + } + if !util.EqualUnsorted(x509req.Subject.Country, spec.Subject.Countries) { + violations = append(violations, "spec.subject.countries") + } + if !util.EqualUnsorted(x509req.Subject.Locality, spec.Subject.Localities) { + violations = append(violations, "spec.subject.localities") + } + if !util.EqualUnsorted(x509req.Subject.OrganizationalUnit, spec.Subject.OrganizationalUnits) { + violations = append(violations, "spec.subject.organizationalUnits") + } + if !util.EqualUnsorted(x509req.Subject.PostalCode, spec.Subject.PostalCodes) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.Province, spec.Subject.Provinces) { + violations = append(violations, "spec.subject.postCodes") + } + if !util.EqualUnsorted(x509req.Subject.StreetAddress, spec.Subject.StreetAddresses) { + violations = append(violations, "spec.subject.streetAddresses") + } + if req.Spec.IsCA != spec.IsCA { + violations = append(violations, "spec.isCA") + } + if !util.EqualKeyUsagesUnsorted(req.Spec.Usages, spec.Usages) { + violations = append(violations, "spec.usages") + } + if spec.Duration != nil && req.Spec.Duration != nil && + spec.Duration.Duration != req.Spec.Duration.Duration { + violations = append(violations, "spec.duration") + } + if !reflect.DeepEqual(spec.IssuerRef, req.Spec.IssuerRef) { + violations = append(violations, "spec.issuerRef") + } + } else { + // we have a LiteralSubject + // parse the subject of the csr in the same way as we parse LiteralSubject and see whether the RDN Sequences match + + var rdnSequenceFromCertificateRequest pkix.RDNSequence + _, err2 := asn1.Unmarshal(x509req.RawSubject, &rdnSequenceFromCertificateRequest) + if err2 != nil { + return nil, err2 + } + + rdnSequenceFromCertificate, err := pki.ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(rdnSequenceFromCertificate, rdnSequenceFromCertificateRequest) { + violations = append(violations, "spec.literalSubject") + } + } + + return violations, nil +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go new file mode 100644 index 0000000..040453b --- /dev/null +++ b/pkg/controller/helper.go @@ -0,0 +1,15 @@ +package controller + +import ( + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +// ResourceNamespace returns the Kubernetes namespace where resources +// created or read by `iss` are located. +func (o IssuerOptions) ResourceNamespace(iss acmapi.GenericIssuer) string { + ns := iss.GetObjectMeta().Namespace + if ns == "" { + ns = o.ClusterResourceNamespace + } + return ns +} diff --git a/pkg/util/kube/pki.go b/pkg/util/kube/pki.go new file mode 100644 index 0000000..6f8055c --- /dev/null +++ b/pkg/util/kube/pki.go @@ -0,0 +1,43 @@ +package kube + +import ( + "context" + "crypto" + + corev1 "k8s.io/api/core/v1" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corelisters "k8s.io/client-go/listers/core/v1" +) + +func SecretTLSKey(ctx context.Context, secretLister corelisters.SecretLister, namespace, name string) (crypto.Signer, error) { + return SecretTLSKeyRef(ctx, secretLister, namespace, name, corev1.TLSPrivateKeyKey) +} + +//SecretTLSKeyRef will fetch the key from the secret. +func SecretTLSKeyRef(ctx context.Context, secretLister corelisters.SecretLister, namespace, name, keyName string) (crypto.Signer, error) { + secret, err := secretLister.Secrets(namespace).Get(name) + if err != nil { + return nil, err + } + + key, _, err := ParseTLSKeyFromSecret(secret, keyName) + if err != nil { + return nil, err + } + return key, nil +} + +func ParseTLSKeyFromSecret(secret *corev1.Secret, keyName string) (crypto.Signer, []byte, error) { + keyBytes, ok := secret.Data[keyName] + if !ok { + return nil, nil, errors.NewInvalidData("no data for %q in secret '%s/%s'", keyName, secret.Namespace, secret.Name) + } + + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return nil, keyBytes, errors.NewInvalidData(err.Error()) + } + return key, keyBytes, nil +} diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index e29bd42..1920afa 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -1,46 +1,75 @@ package pki import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" + "fmt" + "math/big" "net" "net/url" "strings" + "time" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" ) -// URLsFromString parses the urls from the string array -func URLsFromString(urlStrs []string) ([]*url.URL, error) { +func IPAddressesForCertificate(crt *v1.Certificate) []net.IP { + var ipAddresses []net.IP + var ip net.IP + for _, ipName := range crt.Spec.IPAddresses { + ip = net.ParseIP(ipName) + if ip != nil { + ipAddresses = append(ipAddresses, ip) + } + } + return ipAddresses +} + +func URIsForCertificate(crt *v1.Certificate) ([]*url.URL, error) { + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, fmt.Errorf("failed to parse URIs: %s", err) + } + + return uris, nil +} + +func DNSNamesForCertificate(crt *v1.Certificate) ([]string, error) { + _, err := URLsFromStrings(crt.Spec.DNSNames) + if err != nil { + return nil, fmt.Errorf("failed to parse DNSNames: %s", err) + } + + return crt.Spec.DNSNames, nil +} + +func URLsFromStrings(urlStrs []string) ([]*url.URL, error) { var urls []*url.URL var errs []string + for _, urlStr := range urlStrs { url, err := url.Parse(urlStr) if err != nil { errs = append(errs, err.Error()) continue } + urls = append(urls, url) } if len(errs) > 0 { return nil, errors.New(strings.Join(errs, ", ")) } + return urls, nil } -// URLsToString converts the array of *url.URL object to the string array -func URLsToString(urls []*url.URL) []string { - var urlStrs []string - for _, url := range urls { - if urls == nil { - panic("provided url to string is nil") - } - - urlStrs = append(urlStrs, url.String()) - } - - return urlStrs -} - -// IPAddressesToString converts the ip address to the string func IPAddressesToString(ipAddresses []net.IP) []string { var ipNames []string for _, ip := range ipAddresses { @@ -48,3 +77,581 @@ } return ipNames } + +func URLsToString(uris []*url.URL) []string { + var uriStrs []string + for _, uri := range uris { + if uri == nil { + panic("provided uri to string is nil") + } + + uriStrs = append(uriStrs, uri.String()) + } + + return uriStrs +} + +func removeDuplicates(in []string) []string { + var found []string +Outer: + for _, i := range in { + for _, i2 := range found { + if i2 == i { + continue Outer + } + } + found = append(found, i) + } + return found +} + +// OrganizationForCertificate will return the Organization to set for the +// Certificate resource. +// If an Organization is not specifically set, a default will be used. +func OrganizationForCertificate(crt *v1.Certificate) []string { + if crt.Spec.Subject == nil { + return nil + } + return crt.Spec.Subject.Organizations +} + +// SubjectForCertificate will return the Subject from the Certificate resource or an empty one if it is not set +func SubjectForCertificate(crt *v1.Certificate) v1.X509Subject { + if crt.Spec.Subject == nil { + return v1.X509Subject{} + } + + return *crt.Spec.Subject +} + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) + +func BuildKeyUsages(usages []v1.KeyUsage, isCA bool) (ku x509.KeyUsage, eku []x509.ExtKeyUsage, err error) { + var unk []v1.KeyUsage + if isCA { + ku |= x509.KeyUsageCertSign + } + if len(usages) == 0 { + usages = append(usages, v1.DefaultKeyUsages()...) + } + for _, u := range usages { + if kuse, ok := apiutil.KeyUsageType(u); ok { + ku |= kuse + } else if ekuse, ok := apiutil.ExtKeyUsageType(u); ok { + eku = append(eku, ekuse) + } else { + unk = append(unk, u) + } + } + if len(unk) > 0 { + err = fmt.Errorf("unknown key usages: %v", unk) + } + return +} + +func BuildCertManagerKeyUsages(ku x509.KeyUsage, eku []x509.ExtKeyUsage) []v1.KeyUsage { + usages := apiutil.KeyUsageStrings(ku) + usages = append(usages, apiutil.ExtKeyUsageStrings(eku)...) + + return usages +} + +// GenerateCSR will generate a new *x509.CertificateRequest template to be used +// by issuers that utilise CSRs to obtain Certificates. +// The CSR will not be signed, and should be passed to either EncodeCSR or +// to the x509.CreateCertificateRequest function. +func GenerateCSR(crt *v1.Certificate) (*x509.CertificateRequest, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + iPAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + + dnsNames, err := DNSNamesForCertificate(crt) + if err != nil { + return nil, err + } + + uriNames, err := URIsForCertificate(crt) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 && len(crt.Spec.EmailAddresses) == 0 && len(crt.Spec.IPAddresses) == 0 { + return nil, fmt.Errorf("no common name, DNS name, URI SAN, or Email SAN specified on certificate") + } + + pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + // var extraExtensions []pkix.Extension + // if crt.Spec.EncodeUsagesInRequest == nil || *crt.Spec.EncodeUsagesInRequest { + // extraExtensions, err = buildKeyUsagesExtensionsForCertificate(crt) + // if err != nil { + // return nil, err + // } + // } + + // if utilfeature.DefaultFeatureGate.Enabled(feature.UseCertificateRequestBasicConstraints) { + // extension, err := buildBasicConstraintsExtensionsForCertificate(crt.Spec.IsCA) + // if err != nil { + // return nil, err + // } + // extraExtensions = append(extraExtensions, extension) + // } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + RawSubject: rawSubject, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + //ExtraExtensions: extraExtensions, + }, nil + } else { + return &x509.CertificateRequest{ + // Version 0 is the only one defined in the PKCS#10 standard, RFC2986. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc2986#section-4 + Version: 0, + SignatureAlgorithm: sigAlgo, + PublicKeyAlgorithm: pubKeyAlgo, + + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + DNSNames: dnsNames, + IPAddresses: iPAddresses, + URIs: uriNames, + EmailAddresses: crt.Spec.EmailAddresses, + // ExtraExtensions: extraExtensions, + }, nil + } + +} + +// func buildKeyUsagesExtensionsForCertificate(crt *v1.Certificate) ([]pkix.Extension, error) { +// ku, ekus, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) +// if err != nil { +// return nil, fmt.Errorf("failed to build key usages: %w", err) +// } + +// usage, err := buildASN1KeyUsageRequest(ku) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode usages: %w", err) +// } +// asn1ExtendedUsages := []asn1.ObjectIdentifier{} +// for _, eku := range ekus { +// if oid, ok := OIDFromExtKeyUsage(eku); ok { +// asn1ExtendedUsages = append(asn1ExtendedUsages, oid) +// } +// } + +// extraExtensions := []pkix.Extension{usage} +// if len(ekus) > 0 { +// extendedUsage := pkix.Extension{ +// Id: OIDExtensionExtendedKeyUsage, +// } +// extendedUsage.Value, err = asn1.Marshal(asn1ExtendedUsages) +// if err != nil { +// return nil, fmt.Errorf("failed to asn1 encode extended usages: %w", err) +// } + +// extraExtensions = append(extraExtensions, extendedUsage) +// } +// return extraExtensions, nil +// } + +// func buildBasicConstraintsExtensionsForCertificate(isCA bool) (pkix.Extension, error) { + +// basicConstraints := pkix.Extension{ +// Id: OIDExtensionBasicConstraints, +// } + +// constraint := struct { +// IsCA bool +// }{ +// IsCA: isCA, +// } + +// var err error +// basicConstraints.Value, err = asn1.Marshal(constraint) +// if err != nil { +// return pkix.Extension{}, err +// } + +// return basicConstraints, nil +// } + +// GenerateTemplate will create a x509.Certificate for the given Certificate resource. +// This should create a Certificate template that is equivalent to the CertificateRequest +// generated by GenerateCSR. +// The PublicKey field must be populated by the caller. +func GenerateTemplate(crt *v1.Certificate) (*x509.Certificate, error) { + commonName, err := extractCommonName(crt.Spec) + if err != nil { + return nil, err + } + + dnsNames := crt.Spec.DNSNames + ipAddresses := IPAddressesForCertificate(crt) + organization := OrganizationForCertificate(crt) + subject := SubjectForCertificate(crt) + uris, err := URLsFromStrings(crt.Spec.URIs) + if err != nil { + return nil, err + } + keyUsages, extKeyUsages, err := BuildKeyUsages(crt.Spec.Usages, crt.Spec.IsCA) + if err != nil { + return nil, err + } + + if len(commonName) == 0 && len(dnsNames) == 0 && len(ipAddresses) == 0 && len(uris) == 0 && len(crt.Spec.EmailAddresses) == 0 { + return nil, fmt.Errorf("no common name or subject alt names requested on certificate") + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + certDuration := apiutil.DefaultCertDuration(crt.Spec.Duration) + + pubKeyAlgo, _, err := SignatureAlgorithm(crt) + if err != nil { + return nil, err + } + + if isLiteralCertificateSubjectEnabled() && len(crt.Spec.LiteralSubject) > 0 { + rawSubject, err := ParseSubjectStringToRawDerBytes(crt.Spec.LiteralSubject) + if err != nil { + return nil, err + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + RawSubject: rawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } else { + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: pubKeyAlgo, + IsCA: crt.Spec.IsCA, + Subject: pkix.Name{ + Country: subject.Countries, + Organization: organization, + OrganizationalUnit: subject.OrganizationalUnits, + Locality: subject.Localities, + Province: subject.Provinces, + StreetAddress: subject.StreetAddresses, + PostalCode: subject.PostalCodes, + SerialNumber: subject.SerialNumber, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certDuration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsages, + ExtKeyUsage: extKeyUsages, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + EmailAddresses: crt.Spec.EmailAddresses, + }, nil + } +} + +// GenerateTemplate will create a x509.Certificate for the given +// CertificateRequest resource +func GenerateTemplateFromCertificateRequest(cr *v1.CertificateRequest) (*x509.Certificate, error) { + certDuration := apiutil.DefaultCertDuration(cr.Spec.Duration) + keyUsage, extKeyUsage, err := BuildKeyUsages(cr.Spec.Usages, cr.Spec.IsCA) + if err != nil { + return nil, err + } + return GenerateTemplateFromCSRPEMWithUsages(cr.Spec.Request, certDuration, cr.Spec.IsCA, keyUsage, extKeyUsage) +} + +func GenerateTemplateFromCSRPEM(csrPEM []byte, duration time.Duration, isCA bool) (*x509.Certificate, error) { + var ( + ku x509.KeyUsage + eku []x509.ExtKeyUsage + ) + return GenerateTemplateFromCSRPEMWithUsages(csrPEM, duration, isCA, ku, eku) +} + +func GenerateTemplateFromCSRPEMWithUsages(csrPEM []byte, duration time.Duration, isCA bool, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage) (*x509.Certificate, error) { + block, _ := pem.Decode(csrPEM) + if block == nil { + return nil, errors.New("failed to decode csr") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, err + } + + if err := csr.CheckSignature(); err != nil { + return nil, err + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) + } + + return &x509.Certificate{ + // Version must be 2 according to RFC5280. + // A version value of 2 confusingly means version 3. + // This value isn't used by Go at the time of writing. + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1 + Version: 2, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + PublicKey: csr.PublicKey, + IsCA: isCA, + Subject: csr.Subject, + RawSubject: csr.RawSubject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(duration), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + URIs: csr.URIs, + }, nil +} + +// SignCertificate returns a signed *x509.Certificate given a template +// *x509.Certificate crt and an issuer. +// publicKey is the public key of the signee, and signerKey is the private +// key of the signer. +// It returns a PEM encoded copy of the Certificate as well as a *x509.Certificate +// which can be used for reading the encoded values. +func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, publicKey crypto.PublicKey, signerKey interface{}) ([]byte, *x509.Certificate, error) { + derBytes, err := x509.CreateCertificate(rand.Reader, template, issuerCert, publicKey, signerKey) + + if err != nil { + return nil, nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, nil, fmt.Errorf("error decoding DER certificate bytes: %s", err.Error()) + } + + pemBytes := bytes.NewBuffer([]byte{}) + err = pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return nil, nil, fmt.Errorf("error encoding certificate PEM: %s", err.Error()) + } + + return pemBytes.Bytes(), cert, err +} + +// // SignCSRTemplate signs a certificate template usually based upon a CSR. This +// // function expects all fields to be present in the certificate template, +// // including it's public key. +// // It returns the PEM bundle containing certificate data and the CA data, encoded in PEM format. +// func SignCSRTemplate(caCerts []*x509.Certificate, caKey crypto.Signer, template *x509.Certificate) (PEMBundle, error) { +// if len(caCerts) == 0 { +// return PEMBundle{}, errors.New("no CA certificates given to sign CSR template") +// } + +// issuingCACert := caCerts[0] + +// _, cert, err := SignCertificate(template, issuingCACert, template.PublicKey, caKey) +// if err != nil { +// return PEMBundle{}, err +// } + +// bundle, err := ParseSingleCertificateChain(append(caCerts, cert)) +// if err != nil { +// return PEMBundle{}, err +// } + +// return bundle, nil +// } + +// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template. +// It returns a DER encoded signed CSR. +func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) { + derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error()) + } + + return derBytes, nil +} + +// EncodeX509 will encode a single *x509.Certificate into PEM format. +func EncodeX509(cert *x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + + return caPem.Bytes(), nil +} + +// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain. +// Self-signed certificates are not included as per +// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 +// Certificates are output in the order they're given; if the input is not ordered +// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid +// for use in TLS. +func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) { + caPem := bytes.NewBuffer([]byte{}) + for _, cert := range certs { + if cert == nil { + continue + } + + if cert.CheckSignatureFrom(cert) == nil { + // Don't include self-signed certificate + continue + } + + err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return caPem.Bytes(), nil +} + +// SignatureAlgorithm will determine the appropriate signature algorithm for +// the given certificate. +// Adapted from https://github.com/cloudflare/cfssl/blob/master/csr/csr.go#L102 +func SignatureAlgorithm(crt *v1.Certificate) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + var sigAlgo x509.SignatureAlgorithm + var pubKeyAlgo x509.PublicKeyAlgorithm + var specAlgorithm v1.PrivateKeyAlgorithm + if crt.Spec.PrivateKey != nil { + specAlgorithm = crt.Spec.PrivateKey.Algorithm + } + switch specAlgorithm { + case v1.PrivateKeyAlgorithm(""): + // If keyAlgorithm is not specified, we default to rsa with keysize 2048 + pubKeyAlgo = x509.RSA + sigAlgo = x509.SHA256WithRSA + case v1.RSAKeyAlgorithm: + pubKeyAlgo = x509.RSA + switch { + case crt.Spec.PrivateKey.Size >= 4096: + sigAlgo = x509.SHA512WithRSA + case crt.Spec.PrivateKey.Size >= 3072: + sigAlgo = x509.SHA384WithRSA + case crt.Spec.PrivateKey.Size >= 2048: + sigAlgo = x509.SHA256WithRSA + // 0 == not set + case crt.Spec.PrivateKey.Size == 0: + sigAlgo = x509.SHA256WithRSA + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported rsa keysize specified: %d. min keysize %d", crt.Spec.PrivateKey.Size, MinRSAKeySize) + } + case v1.Ed25519KeyAlgorithm: + pubKeyAlgo = x509.Ed25519 + sigAlgo = x509.PureEd25519 + case v1.ECDSAKeyAlgorithm: + pubKeyAlgo = x509.ECDSA + switch crt.Spec.PrivateKey.Size { + case 521: + sigAlgo = x509.ECDSAWithSHA512 + case 384: + sigAlgo = x509.ECDSAWithSHA384 + case 256: + sigAlgo = x509.ECDSAWithSHA256 + case 0: + sigAlgo = x509.ECDSAWithSHA256 + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported ecdsa keysize specified: %d", crt.Spec.PrivateKey.Size) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, fmt.Errorf("unsupported algorithm specified: %s. should be either 'ecdsa' or 'rsa", crt.Spec.PrivateKey.Algorithm) + } + return pubKeyAlgo, sigAlgo, nil +} + +func extractCommonName(spec v1.CertificateSpec) (string, error) { + var commonName = spec.CommonName + if isLiteralCertificateSubjectEnabled() && len(spec.LiteralSubject) > 0 { + commonName = "" + sequence, err := ParseSubjectStringToRdnSequence(spec.LiteralSubject) + if err != nil { + return "", err + } + + for _, rdns := range sequence { + for _, atv := range rdns { + if atv.Type.Equal(OIDConstants.CommonName) { + if str, ok := atv.Value.(string); ok { + commonName = str + } + } + } + } + } + + return commonName, nil + +} + +func isLiteralCertificateSubjectEnabled() bool { + return false + //return utilfeature.DefaultFeatureGate.Enabled(feature.LiteralCertificateSubject) +} diff --git a/pkg/util/pki/generate.go b/pkg/util/pki/generate.go index 911a33c..de1395b 100644 --- a/pkg/util/pki/generate.go +++ b/pkg/util/pki/generate.go @@ -4,12 +4,32 @@ "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + v1 "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +const ( + // MinRSAKeySize is the minimum RSA keysize allowed to be generated by the + // generator functions in this package. + MinRSAKeySize = 2048 + + // MaxRSAKeySize is the maximum RSA keysize allowed to be generated by the + // generator functions in this package. + MaxRSAKeySize = 8192 + + // ECCurve256 represents a secp256r1 / prime256v1 / NIST P-256 ECDSA key. + ECCurve256 = 256 + // ECCurve384 represents a secp384r1 / NIST P-384 ECDSA key. + ECCurve384 = 384 + // ECCurve521 represents a secp521r1 / NIST P-521 ECDSA key. + ECCurve521 = 521 ) // EncodePrivateKey will encode a given crypto.PrivateKey by first inspecting @@ -63,3 +83,119 @@ block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes} return pem.EncodeToMemory(block), nil } + +// PublicKeyMatchesCSR can be used to verify the given public key matches the +// public key in the given x509.CertificateRequest. +// Returns false and no error if the given public key is *not* the same as the CSR's key +// Returns true and no error if the given public key *is* the same as the CSR's key +// Returns an error if the CSR's key type cannot be determined (i.e. non RSA/ECDSA keys) +func PublicKeyMatchesCSR(check crypto.PublicKey, csr *x509.CertificateRequest) (bool, error) { + return PublicKeysEqual(csr.PublicKey, check) +} + +// PublicKeysEqual compares two given public keys for equality. +// The definition of "equality" depends on the type of the public keys. +// Returns true if the keys are the same, false if they differ or an error if +// the key type of `a` cannot be determined. +func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) { + switch pub := a.(type) { + case *rsa.PublicKey: + return pub.Equal(b), nil + case *ecdsa.PublicKey: + return pub.Equal(b), nil + case ed25519.PublicKey: + return pub.Equal(b), nil + default: + return false, fmt.Errorf("unrecognised public key type: %T", a) + } +} + +// GeneratePrivateKeyForCertificate will generate a private key suitable for +// the provided cert-manager Certificate resource, taking into account the +// parameters on the provided resource. +// The returned key will either be RSA or ECDSA. +func GeneratePrivateKeyForCertificate(crt *v1.Certificate) (crypto.Signer, error) { + crt = crt.DeepCopy() + if crt.Spec.PrivateKey == nil { + crt.Spec.PrivateKey = &v1.CertificatePrivateKey{} + } + switch crt.Spec.PrivateKey.Algorithm { + case v1.PrivateKeyAlgorithm(""), v1.RSAKeyAlgorithm: + keySize := MinRSAKeySize + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateRSAPrivateKey(keySize) + case v1.ECDSAKeyAlgorithm: + keySize := ECCurve256 + + if crt.Spec.PrivateKey.Size > 0 { + keySize = crt.Spec.PrivateKey.Size + } + + return GenerateECPrivateKey(keySize) + case v1.Ed25519KeyAlgorithm: + return GenerateEd25519PrivateKey() + default: + return nil, fmt.Errorf("unsupported private key algorithm specified: %s", crt.Spec.PrivateKey.Algorithm) + } +} + +// GenerateRSAPrivateKey will generate a RSA private key of the given size. +// It places restrictions on the minimum and maximum RSA keysize. +func GenerateRSAPrivateKey(keySize int) (*rsa.PrivateKey, error) { + // Do not allow keySize < 2048 + // https://en.wikipedia.org/wiki/Key_size#cite_note-twirl-14 + if keySize < MinRSAKeySize { + return nil, fmt.Errorf("weak rsa key size specified: %d. minimum key size: %d", keySize, MinRSAKeySize) + } + if keySize > MaxRSAKeySize { + return nil, fmt.Errorf("rsa key size specified too big: %d. maximum key size: %d", keySize, MaxRSAKeySize) + } + + return rsa.GenerateKey(rand.Reader, keySize) +} + +// GenerateECPrivateKey will generate an ECDSA private key of the given size. +// It can be used to generate 256, 384 and 521 sized keys. +func GenerateECPrivateKey(keySize int) (*ecdsa.PrivateKey, error) { + var ecCurve elliptic.Curve + + switch keySize { + case ECCurve256: + ecCurve = elliptic.P256() + case ECCurve384: + ecCurve = elliptic.P384() + case ECCurve521: + ecCurve = elliptic.P521() + default: + return nil, fmt.Errorf("unsupported ecdsa key size specified: %d", keySize) + } + + return ecdsa.GenerateKey(ecCurve, rand.Reader) +} + +// GenerateEd25519PrivateKey will generate an Ed25519 private key +func GenerateEd25519PrivateKey() (ed25519.PrivateKey, error) { + + _, prvkey, err := ed25519.GenerateKey(rand.Reader) + + return prvkey, err +} + +// PublicKeyForPrivateKey will return the crypto.PublicKey for the given +// crypto.PrivateKey. It only supports RSA and ECDSA keys. +func PublicKeyForPrivateKey(pk crypto.PrivateKey) (crypto.PublicKey, error) { + switch k := pk.(type) { + case *rsa.PrivateKey: + return k.Public(), nil + case *ecdsa.PrivateKey: + return k.Public(), nil + case ed25519.PrivateKey: + return k.Public(), nil + default: + return nil, fmt.Errorf("unknown private key type: %T", pk) + } +} diff --git a/pkg/util/pki/keyusage.go b/pkg/util/pki/keyusage.go new file mode 100644 index 0000000..6404b4d --- /dev/null +++ b/pkg/util/pki/keyusage.go @@ -0,0 +1,135 @@ +package pki + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" +) + +// Copied from x509.go +var ( + OIDExtensionKeyUsage = []int{2, 5, 29, 15} + OIDExtensionExtendedKeyUsage = []int{2, 5, 29, 37} + OIDExtensionBasicConstraints = []int{2, 5, 29, 19} +) + +// RFC 5280, 4.2.1.12 Extended Key Usage +// +// anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 } +// +// id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } +// +// id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } +// id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } +// id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 } +// id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 } +// id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 } +// id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } +var ( + oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} + oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} + oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} + oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} + oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} + oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} + oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} + oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} + oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} + oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} + oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} + oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} + oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} + oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1} +) + +// extKeyUsageOIDs contains the mapping between an ExtKeyUsage and its OID. +var extKeyUsageOIDs = []struct { + extKeyUsage x509.ExtKeyUsage + oid asn1.ObjectIdentifier +}{ + {x509.ExtKeyUsageAny, oidExtKeyUsageAny}, + {x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth}, + {x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth}, + {x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning}, + {x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection}, + {x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem}, + {x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel}, + {x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser}, + {x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping}, + {x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning}, + {x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto}, + {x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto}, + {x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning}, + {x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning}, +} + +// OIDFromExtKeyUsage returns the ASN1 Identifier for a x509.ExtKeyUsage +func OIDFromExtKeyUsage(eku x509.ExtKeyUsage) (oid asn1.ObjectIdentifier, ok bool) { + for _, pair := range extKeyUsageOIDs { + if eku == pair.extKeyUsage { + return pair.oid, true + } + } + return +} + +func ExtKeyUsageFromOID(oid asn1.ObjectIdentifier) (eku x509.ExtKeyUsage, ok bool) { + for _, pair := range extKeyUsageOIDs { + if oid.Equal(pair.oid) { + return pair.extKeyUsage, true + } + } + return +} + +// asn1BitLength returns the bit-length of bitString by considering the +// most-significant bit in a byte to be the "first" bit. This convention +// matches ASN.1, but differs from almost everything else. +func asn1BitLength(bitString []byte) int { + bitLen := len(bitString) * 8 + + for i := range bitString { + b := bitString[len(bitString)-i-1] + + for bit := uint(0); bit < 8; bit++ { + if (b>>bit)&1 == 1 { + return bitLen + } + bitLen-- + } + } + + return 0 +} + +// Copied from x509.go +func reverseBitsInAByte(in byte) byte { + b1 := in>>4 | in<<4 + b2 := b1>>2&0x33 | b1<<2&0xcc + b3 := b2>>1&0x55 | b2<<1&0xaa + return b3 +} + +// Adapted from x509.go +func buildASN1KeyUsageRequest(usage x509.KeyUsage) (pkix.Extension, error) { + OIDExtensionKeyUsage := pkix.Extension{ + Id: OIDExtensionKeyUsage, + } + var a [2]byte + a[0] = reverseBitsInAByte(byte(usage)) + a[1] = reverseBitsInAByte(byte(usage >> 8)) + + l := 1 + if a[1] != 0 { + l = 2 + } + + bitString := a[:l] + var err error + OIDExtensionKeyUsage.Value, err = asn1.Marshal(asn1.BitString{Bytes: bitString, BitLength: asn1BitLength(bitString)}) + if err != nil { + return pkix.Extension{}, err + } + + return OIDExtensionKeyUsage, nil +} diff --git a/pkg/util/pki/parse.go b/pkg/util/pki/parse.go index e8ead01..3c7399d 100644 --- a/pkg/util/pki/parse.go +++ b/pkg/util/pki/parse.go @@ -3,8 +3,12 @@ import ( "crypto" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" + "github.com/go-ldap/ldap/v3" + errors "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/errors" ) @@ -99,3 +103,74 @@ } } + +var OIDConstants = struct { + Country []int + Organization []int + OrganizationalUnit []int + CommonName []int + SerialNumber []int + Locality []int + Province []int + StreetAddress []int +}{ + Country: []int{2, 5, 4, 6}, + Organization: []int{2, 5, 4, 10}, + OrganizationalUnit: []int{2, 5, 4, 11}, + CommonName: []int{2, 5, 4, 3}, + SerialNumber: []int{2, 5, 4, 5}, + Locality: []int{2, 5, 4, 7}, + Province: []int{2, 5, 4, 8}, + StreetAddress: []int{2, 5, 4, 9}, +} + +// Copied from pkix.attributeTypeNames and inverted. (Sadly it is private.) +// Source: https://cs.opensource.google/go/go/+/refs/tags/go1.18.2:src/crypto/x509/pkix/pkix.go;l=26 +var attributeTypeNames = map[string][]int{ + "C": OIDConstants.Country, + "O": OIDConstants.Organization, + "OU": OIDConstants.OrganizationalUnit, + "CN": OIDConstants.CommonName, + "SERIALNUMBER": OIDConstants.SerialNumber, + "L": OIDConstants.Locality, + "ST": OIDConstants.Province, + "STREET": OIDConstants.StreetAddress, +} + +func ParseSubjectStringToRdnSequence(subject string) (pkix.RDNSequence, error) { + + dns, err := ldap.ParseDN(subject) + if err != nil { + return nil, err + } + + // Traverse the parsed RDNSequence in REVERSE order as RDNs in String format are expected to be written in reverse order. + // Meaning, a string of "CN=Foo,OU=Bar,O=Baz" actually should have "O=Baz" as the first element in the RDNSequence. + var rdns pkix.RDNSequence + for i := range dns.RDNs { + ldapRelativeDN := dns.RDNs[len(dns.RDNs)-i-1] + + var atvs []pkix.AttributeTypeAndValue + for _, ldapATV := range ldapRelativeDN.Attributes { + + atvs = append(atvs, pkix.AttributeTypeAndValue{ + Type: attributeTypeNames[ldapATV.Type], + Value: ldapATV.Value, + }) + + } + rdns = append(rdns, atvs) + } + return rdns, nil + +} + +func ParseSubjectStringToRawDerBytes(subject string) ([]byte, error) { + rdnSequenceFromLiteralString, err := ParseSubjectStringToRdnSequence(subject) + if err != nil { + return nil, err + } + + return asn1.Marshal(rdnSequenceFromLiteralString) + +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 0000000..2d9c313 --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,51 @@ +package util + +import ( + "sort" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" +) + +func EqualUnsorted(s1 []string, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + s1_2, s2_2 := make([]string, len(s1)), make([]string, len(s2)) + copy(s1_2, s1) + copy(s2_2, s2) + sort.Strings(s1_2) + sort.Strings(s2_2) + for i, s := range s1_2 { + if s != s2_2[i] { + return false + } + } + return true +} + +// Test for equal KeyUsage slices even if unsorted +func EqualKeyUsagesUnsorted(s1, s2 []acmapi.KeyUsage) bool { + if len(s1) != len(s2) { + return false + } + s1_2, s2_2 := make([]string, len(s1)), make([]string, len(s2)) + // we may want to implement a sort interface here instead of []byte conversion + for i := range s1 { + s1_2[i] = string(s1[i]) + s2_2[i] = string(s2[i]) + } + + sort.SliceStable(s1_2, func(i, j int) bool { + return s1_2[i] < s1_2[j] + }) + sort.SliceStable(s2_2, func(i, j int) bool { + return s2_2[i] < s2_2[j] + }) + + for i, s := range s1_2 { + if s != s2_2[i] { + return false + } + } + return true +}