Repository: jetstack/cert-manager
Pull request #4598 (2021-11-16 09:02:14)

Added privateKey additional format option
**What this PR does / why we need it**:

This PR add a new `additionalOutputFormats` option in the certificates crd.
For now additional supported formats are :
- `DER` this will create a new entry `tls.der` in the kubernetes secret with the private key in DER format (binary).
- `CombindedPEM` this will create a new entry `tls.pem` in the kubernetes secret with the concatenation of the private key and the certificate.

**Which issue this PR fixes** :

 fixes  #843 (see https://github.com/jetstack/cert-manager/issues/843#issuecomment-566054175)


**Use-case : Postgres JBDC driver (lower than 42.2.8)** :

In recent versions of the Postgres JBDC driver (42.2.9 and above, which was released in Dec 2019), you can use the PKCS#12 format to load the private key and certificate. cert-manager supports PKCS#12 since 0.14, which means the integration with cert-manager works well. Postgresql 13+ [is required](https://www.postgresql.org/docs/13/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD) to accept the `sslpassword` parameter to unlock the PKCS#12 keystore.

But if you are stuck with a version of the Postgres JDBC driver that is 42.2.9 and below, then the only way is to use the sslkey configuration field. The sslkey requires the path to a file containing the private key PKCS#8-formated and DER-encoded.

For example, I've succeffully builded and deployed this version in my cluster and used the new `key.der` private key file to connect to my postgresql database with mTLS :

```
url: jdbc:postgresql://postgresql.database.svc:5432/${postgresql.database}?ssl=true&sslmode=verify-ca&sslrootcert=/certs/ca.crt&sslcert=/certs/tls.crt&sslkey=/certs/key.der&ApplicationName=${spring.application.name}
```

With pg_hba.conf like 
```
hostssl mydatabase all 10.0.0.0/8 cert map=applications
```

and pg_ident.conf 
```
applications /^(.*)\.mynamespace\.svc$ \1
```

**Release note**:
```release-note
Added additionalOutputFormats parameter to allow `DER` (binary) and `CombinedPEM` (key + cert bundle) formats.
```


jetstack-bot: Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

:memo: **Please follow instructions in the [contributing guide](https://github.com/jetstack/cert-manager/blob/master/CONTRIBUTING.md) to update your commits with the DCO**

Full details of the Developer Certificate of Origin can be found at [developercertificate.org](https://developercertificate.org/).

**The list of commits missing DCO signoff**:

- [9d4d85f](https://github.com/jetstack/cert-manager/commits/9d4d85f67e8b98a4c8a43abe9c6ef28a2524f382) added privateKey with der format option

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>

jetstack-bot: Hi @seuf. Thanks for your PR.

I'm waiting for a [jetstack](https://github.com/orgs/jetstack/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/jetstack/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=jetstack%2Fcert-manager).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

jetstack-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/jetstack/cert-manager/pull/4598#" title="Author self-approved">seuf</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **munnerz** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @munnerz` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=jetstack%2Fcert-manager).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/jetstack/cert-manager/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["munnerz"]} -->
jetstack-bot: Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

:memo: **Please follow instructions in the [contributing guide](https://github.com/jetstack/cert-manager/blob/master/CONTRIBUTING.md) to update your commits with the DCO**

Full details of the Developer Certificate of Origin can be found at [developercertificate.org](https://developercertificate.org/).

**The list of commits missing DCO signoff**:

- [9d4d85f](https://github.com/jetstack/cert-manager/commits/9d4d85f67e8b98a4c8a43abe9c6ef28a2524f382) added privateKey with der format option

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>

SgtCoDFish: /kind feature
/ok-to-test
munnerz: I think the strategy to implement this needs careful consideration - I highlighted a couple of cases, but you should also consider what happens if a user sets this on an existing Certificate. With this implementation, the Secret resource won't be updated until a re-issuance occurs.

A more intelligent controller could read the `tls.key` entry and actually extract the DER encoded contents out.

Have you considered building this as an entirely external controller that watches for secrets of type `kubernetes.io/tls`, reads `tls.pem` and writes `tls.der` back into the Secret? This would probably be far simpler to write (less complexity in the cert-manager codebase to deal with) as well as being entirely generic too.
munnerz: I'm also conscious that #843 actually references a whole host of alternative formats, not just DER. We need to consider what the implementation plan for the others mentioned in that issue is (in terms of API surface) to ensure we don't create an 'awkward' API.

The general consensus in #843 was that it's probably easier to implement this stuff externally due to the huge number of formats (and then additional options on those formats, e.g. passwords to encrypt bundles etc)
seuf: > Have you considered building this as an entirely external controller that watches for secrets of type `kubernetes.io/tls`, reads `tls.pem` and writes `tls.der` back into the Secret? This would probably be far simpler to write (less complexity in the cert-manager codebase to deal with) as well as being entirely generic too.
Do you mean a controller like https://github.com/maelvls/secret-transform ?
The idea is to watch kubernetes secrets and add new entry tls.der each time a tls.key is present..
I've considered this option but it mean a new deployment + rbac + config, etc...  This PR just add a single boolean parameter in the certificate crd and do the job :sunglasses: 
munnerz: > > Have you considered building this as an entirely external controller that watches for secrets of type `kubernetes.io/tls`, reads `tls.pem` and writes `tls.der` back into the Secret? This would probably be far simpler to write (less complexity in the cert-manager codebase to deal with) as well as being entirely generic too.
> 
> Do you mean a controller like https://github.com/maelvls/secret-transform ? The idea is to watch kubernetes secrets and add new entry tls.der each time a tls.key is present.. I've considered this option but it mean a new deployment + rbac + config, etc... This PR just add a single boolean parameter in the certificate crd and do the job 😎

Yep precisely 😄 the difference is that it's not quite that simple when you also account for keeping the field up to date (e.g. if the Secret ever changes), flipping the field from true->false, and also how we accommodate other secret formats. We've typically err'd on the side of allowing users to extend the functionality of the system through other controllers rather than making our own monolithic controller for all different types of transformations that may need to be applied.
SgtCoDFish: ## Overall

I think it would be totally valid and a good improvement to add additional keys to the secret via a `format` or `additionalKeyFormats` or whatever. I fully agree with [this comment](https://github.com/jetstack/cert-manager/pull/4598#discussion_r750067380), though, that a different name than `WithDerFormat` would be appropriate and would be easier to extend in the future if we want to add more private key types.

Crucially, the private key is information that cert-manager already has (i.e. it already knows the private key when it's writing the secret) and it doesn't seem to be a massive burden in terms of memory, CPU, storage or network requests to add this functionality. Ensuring that the cert/key is written in a format which is useful to people is a reasonable thing to add to cert-manager and is worth expanding the API for. I don't think there are enough private key formats in the wild that it would become a burden for us to add support for more. It's quite distinct from, say, ACME challenge solvers or `Issuer`s, both of which I think absolutely should be implemented using new controllers.

cert-manager writing the DER formatted secret also avoids having a "DER controller" in the cluster which has permissions to read private keys, which would increase attack surface. It also avoids having to run another thing which is valuable in and of itself, as @seuf [alluded to](https://github.com/jetstack/cert-manager/pull/4598#issuecomment-970232829) earlier.

Security of a "DER controller" aside, I feel the UX would be better if cert-manager itself wrote the DER key to the secret. Having a "DER controller" augment the secret after cert-manager writes it means that for a pod which needs the DER key, the secret won't be ready until the second controller is finished. The question "is the secret ready?" becomes harder to answer.

(An alternative approach would be that the "DER controller" creates a second secret derived from the secret which cert-manager writes, but this creates another place where one private key is stored, making audits harder and complicating things from an operational perspective)

## This PR

I think for me to be able to approve this PR, it would need:

1. A different name to `WithDerFormat`
2. At least one e2e test which validates this new behaviour
3. An answer to the issue raised in [this comment](https://github.com/jetstack/cert-manager/pull/4598#discussion_r750068463). That's a sharp edge which could catch people out; maybe a warning would be enough, but ideally the `tls.der` key would be removed.
4. Ideally, for reissuance to be triggered by `WithDerFormat`'s value being changed.
munnerz: So I think the API format may want to encompass additional options for potential other formats in future too, e.g.:

```
privateKey:
    algorithm: ECDSA
    size: 256
    additionalKeyFormats:
    - type: DER
    - type: some-type-with-options
      someAdditionalOption: xyz
```

> Ideally, for reissuance to be triggered by WithDerFormat's value being changed.

This is something I'm less certain on, as the 'keystore' stuff we have does *not* do this. Right now we are purely driven by `tls.key` and `tls.crt`.

Another thing to consider that makes it harder for us to _remove_ `tls.der` from the Secret if this option is later disabled, is that some people may be running with a similar external controller to do this today, and if we remove this field we may 'fight' this controller (by having us fully manage and remove the key when not needed is effectively a breaking change to some).
SgtCoDFish: > I think the API format may want to encompass additional options

An array of objects would definitely be more extensible than an array of strings, yeah; could also allow people to change the name of the key we add to the secret, and to tweak parameters (like, say, p12 PBKDF2 iterations).

> This is something I'm less certain on, as the 'keystore' stuff we have does not do this. Right now we are purely driven by tls.key and tls.crt.

Fair point, yeah! Maybe it doesn't _need_ to reissue; there's always `cmctl renew`. I'll update my above comment.

> Another thing to consider that makes it harder for us to remove tls.der...

I think it's always reasonable for us to overwrite the secret `Data` in its entirety on re-issuance. I accept and understand that it _could_ break someone but if a pod relies on a secret being augmented with an additional key by a non-cert-manager controller after the secret is written, it _must_ have logic in place to wait for the augmentation to complete or else it's always at risk to breakage.

This comes back to what I mentioned before about the question "is the secret ready?" becoming harder to answer. I think when cert-manager has written a secret after issuance, the answer to "is the secret ready?" should always be "yes" as far as cert-manager is concerned. If a user adds a custom controller which changes the answer to "maybe not yet", I think it's entirely the user's responsibility to add logic to wait for the answer to become "yes" again.
seuf: > > > Have you considered building this as an entirely external controller that watches for secrets of type `kubernetes.io/tls`, reads `tls.pem` and writes `tls.der` back into the Secret? This would probably be far simpler to write (less complexity in the cert-manager codebase to deal with) as well as being entirely generic too.
> > 
> > 
> > Do you mean a controller like https://github.com/maelvls/secret-transform ? The idea is to watch kubernetes secrets and add new entry tls.der each time a tls.key is present.. I've considered this option but it mean a new deployment + rbac + config, etc... This PR just add a single boolean parameter in the certificate crd and do the job sunglasses
> 
> Yep precisely smile the difference is that it's not quite that simple when you also account for keeping the field up to date (e.g. if the Secret ever changes), flipping the field from true->false, and also how we accommodate other secret formats. We've typically err'd on the side of allowing users to extend the functionality of the system through other controllers rather than making our own monolithic controller for all different types of transformations that may need to be applied.

OK, I've forked @maelvls project to adapt it for my needs : https://github.com/seuf/secret-transform
I've builded and deployed it in my cluster, it works.

Regarding this PR I can try to adapt it to be more extensible in the future, but it will need more work.
Is it ok for you if we go with a `privateKey.additionalKeyFormats` to only convert the tls.key in the desired format (`DER` only for now) without removing it in the secret if the option is disabled (to not fight with a potential external controllers)
SgtCoDFish: > Regarding this PR I can try to adapt it to be more extensible in the future, but it will need more work.

Yeah I definitely don't think we need to add other formats than DER to start off with. If you're be interested in making this change, I think it's reasonable to add `privateKey.additionalKeyFormats` something like this to start:

```yaml
                    additionalKeyFormats:
                      description: Other formats in which the key should be written to a secret
                      type: array
                      items:
                        type: object
                        properties:
                          type:
                            description: The type of additional private key format to write
                            type: string
                            enum:
                            - der
```
seuf: OK, I've pushed a new version of the PR with additionalKeyFormats.
Tested in my cluster it works :sunglasses: 
jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | d111e3aaf68becf46939dce0b077752d9af4c202 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1460639235001290752) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 9c9b048eed8c20877b295e54b8d9f89ea2fbe5a1 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1460893000577060864) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 87a1fbea2b4bc811d4beabf096f7c580a06d4f58 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1460976230437228544) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 9b2faece7f242625b7a4dcaf46d3bbd3e368ad1f | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1462804894145581056) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 0468a05fc951a83419cddffd2380cf59e63fc863 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1462886465712164864) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 0468a05fc951a83419cddffd2380cf59e63fc863 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1462886465712164864) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | 0468a05fc951a83419cddffd2380cf59e63fc863 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1462886465909297152) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

:memo: **Please follow instructions in the [contributing guide](https://github.com/jetstack/cert-manager/blob/master/CONTRIBUTING.md) to update your commits with the DCO**

Full details of the Developer Certificate of Origin can be found at [developercertificate.org](https://developercertificate.org/).

**The list of commits missing DCO signoff**:

- [dd3b58c](https://github.com/jetstack/cert-manager/commits/dd3b58c75fda11110d97bf40c847df42d1849bd8) Update E2E test to validate each secret keys

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>

jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-e2e-v1-22 | 0468a05fc951a83419cddffd2380cf59e63fc863 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1462886465909297152) | `/test pull-cert-manager-e2e-v1-22`
pull-cert-manager-bazel | dd3b58c75fda11110d97bf40c847df42d1849bd8 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1463461111083831296) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | dd3b58c75fda11110d97bf40c847df42d1849bd8 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1463461111083831296) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | dd3b58c75fda11110d97bf40c847df42d1849bd8 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1463461111398404096) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
maelvls: Hi! This PR looks very promising.

Looking at the PR description and the issue associated, I was not able to identify the concrete use-case associated with this feature. Would it be possible to add in the PR description a use-case for it? That is going to be useful when writing the release notes so that people know why this feature came in.

I suggest this:

> ### Use-case: Postgres JBDC driver (lower than 42.2.8)
>
> In recent versions of the Postgres JBDC driver (42.2.9 and above, which was released in Dec 2019), you can use the PKCS#12 format to load the private key and certificate. cert-manager supports PKCS#12 since 0.14, which means the integration with cert-manager works well.
>
> But if you are stuck with a version of the Postgres JDBC driver that is 42.2.9 and below, then the only way is to use the [`sslkey`](https://jdbc.postgresql.org/documentation/head/ssl-client.html) configuration field. The `sslkey` requires the path to a file containing the private key PKCS#8-formated and DER-encoded.

I am not a user of Postgres JBDC driver myself, please let me know if that makes sense.
seuf: > I am not a user of Postgres JBDC driver myself, please let me know if that makes sense.

You're right that's exactly why I am doing this PR (we are stuck on PG 11). The version of the [libpq](https://www.postgresql.org/docs/13/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD) must be 13+ to accept the `sslpassword` option and unlock the pkcs12 keystore.

seuf: Hi, I've updated the PR description with the use case for postgresql (and a configuration example of postgresql hba).
I've also renamed `tls.der` in `key.der` :smiley: 

jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | b4d25fc3f419b09836338c24c609a2da56cf2cd3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1464200298581987328) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | b4d25fc3f419b09836338c24c609a2da56cf2cd3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1464200298581987328) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | b4d25fc3f419b09836338c24c609a2da56cf2cd3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1464200298934308864) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
munnerz: Regarding the feature flagging - we're going to need to wire the `--feature-gates` flag into the webhook component and re-jig how we register features against different components if we want to have this all feature gated. I'll put a PR up shortly to do this, and you'll then be able to build on that more easily 😄
jetstack-bot: Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

:memo: **Please follow instructions in the [contributing guide](https://github.com/jetstack/cert-manager/blob/master/CONTRIBUTING.md) to update your commits with the DCO**

Full details of the Developer Certificate of Origin can be found at [developercertificate.org](https://developercertificate.org/).

**The list of commits missing DCO signoff**:

- [1e7587a](https://github.com/jetstack/cert-manager/commits/1e7587a1d61f790e1956dc8a95345eff9c2a2b29) impletemnts JoshVanL changes request
- [718f9ce](https://github.com/jetstack/cert-manager/commits/718f9cefd18b028e6ea7853c098467a357edf72c) update bazel

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>

seuf: Hi @JoshVanL I've implemented your change requests. 
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | b4d25fc3f419b09836338c24c609a2da56cf2cd3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1464200298581987328) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | e5f6e304f7bf1911c30d52666e5115f2c33d328d | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1466885367230631936) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-e2e-v1-22 | e5f6e304f7bf1911c30d52666e5115f2c33d328d | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1466885367230631936) | `/test pull-cert-manager-e2e-v1-22`
pull-cert-manager-bazel | e5f6e304f7bf1911c30d52666e5115f2c33d328d | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1466885367092219904) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-e2e-v1-22 | e5f6e304f7bf1911c30d52666e5115f2c33d328d | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1466885367230631936) | `/test pull-cert-manager-e2e-v1-22`
pull-cert-manager-bazel | 84e43de71d646401ed7cf3f41f5e404ea462de3b | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1467789393560342528) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 84e43de71d646401ed7cf3f41f5e404ea462de3b | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1467789393560342528) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | 84e43de71d646401ed7cf3f41f5e404ea462de3b | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1467789393862332416) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-e2e-v1-22 | 84e43de71d646401ed7cf3f41f5e404ea462de3b | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1467789393862332416) | `/test pull-cert-manager-e2e-v1-22`
pull-cert-manager-bazel | 97aba355baa773f583d5922ebcc39195bdb135f3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1468142182056595456) | `/test pull-cert-manager-bazel`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 97aba355baa773f583d5922ebcc39195bdb135f3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1468142182056595456) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | 97aba355baa773f583d5922ebcc39195bdb135f3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1468142182358585344) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-bazel | 97aba355baa773f583d5922ebcc39195bdb135f3 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-bazel/1468142182056595456) | `/test pull-cert-manager-bazel`
pull-cert-manager-e2e-v1-22 | 5d09b4e22195702240229a487a2f7912f96ed194 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1468236016425897984) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cert-manager-e2e-v1-22 | 28a7a985678f9b2f97ec88430614aa9d3d9663a4 | [link](https://prow.build-infra.jetstack.net/view/gs/jetstack-logs/pr-logs/pull/jetstack_cert-manager/4598/pull-cert-manager-e2e-v1-22/1468309243718275072) | `/test pull-cert-manager-e2e-v1-22`

[Full PR test history](https://prow.build-infra.jetstack.net/pr-history?org=jetstack&repo=cert-manager&pr=4598). [Your PR dashboard](https://jetstack-build-infra.appspot.com/pr/seuf). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#github-issues-for-known-flakes) an [open issue](https://github.com/jetstack/cert-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jetstack-bot: @seuf: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
jetstack-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/jetstack/cert-manager/pull/4598#" title="Author self-approved">seuf</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **wallrj** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @wallrj` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=jetstack%2Fcert-manager).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/jetstack/cert-manager/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["wallrj"]} -->
seuf: Hi @SgtCoDFish I've updated the PR with fix you suggested. Thanks again for the review.
I've just tested it in my cluster and it works as expected (tested both CombinedPem and DER formats) :smiley: 
SgtCoDFish: > Hi @SgtCoDFish I've updated the PR with fix you suggested. Thanks again for the review. I've just tested it in my cluster and it works as expected (tested both CombinedPem and DER formats) smiley

Thanks @seuf - I've taken another look and I've got a commit ready to fix a couple of other typos and suggest some name changes.

If you give me permission to push to your fork of cert-manager, I can push directly. Otherwise, I've attached a patch you could use with `git apply`.

[spelling-rename-etc.txt](https://github.com/jetstack/cert-manager/files/7848824/spelling-rename-etc.txt)

seuf: @SgtCoDFish I've git applied your changed (and squashed the commit). 
I've also send an invite to my forked repo if you want to add more changes.
seuf: I've updated comments variables and added more unit tests as you suggested.
Regarding the e2e tests, the content of the `tls-combined.pem` or `key.der` are not verified. This is done in unit tests.

Patch:
diff --git a/deploy/crds/crd-certificates.yaml b/deploy/crds/crd-certificates.yaml
index 24ba4cc07..d57717f11 100644
--- a/deploy/crds/crd-certificates.yaml
+++ b/deploy/crds/crd-certificates.yaml
@@ -67,6 +67,21 @@ spec:
                 - issuerRef
                 - secretName
               properties:
+                additionalOutputFormats:
+                  description: AdditionalOutputFormats allows for requests of additional output formats of the private key and the certificate to be written to the secret. This is an Alpha Feature and should be enabled with --feature-gates option.
+                  type: array
+                  items:
+                    description: AdditionalOutputFormat wraps an additional output format type
+                    type: object
+                    required:
+                      - type
+                    properties:
+                      type:
+                        description: OutputFormatType specifies which additional output formats should be added to Kubernetes secrets. Allowed values are `DER` or `CombinedPEM`. When Type is set to `DER` an additional entry `key.der` will be created in the secret containing the binary format of the key. When Type is set to `CombinedPEM` an additional entry `tls-combined.pem` will be created in the secret containing the PEM formatted certificate chain and key (tls.key + tls.crt concatenated)
+                        type: string
+                        enum:
+                          - DER
+                          - CombinedPEM
                 commonName:
                   description: '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. This value is ignored by TLS clients when any subject alt name is set. This is x509 behaviour: https://tools.ietf.org/html/rfc6125#section-6.4.4'
                   type: string
diff --git a/devel/addon/certmanager/install.sh b/devel/addon/certmanager/install.sh
index 201824e2d..803b97c75 100755
--- a/devel/addon/certmanager/install.sh
+++ b/devel/addon/certmanager/install.sh
@@ -23,7 +23,7 @@ NAMESPACE="${NAMESPACE:-cert-manager}"
 # Release name to use with Helm
 RELEASE_NAME="${RELEASE_NAME:-cert-manager}"
 # Default feature gates to enable
-FEATURE_GATES="${FEATURE_GATES:-ExperimentalCertificateSigningRequestControllers=true,ExperimentalGatewayAPISupport=true}"
+FEATURE_GATES="${FEATURE_GATES:-ExperimentalCertificateSigningRequestControllers=true,ExperimentalGatewayAPISupport=true,AdditionalCertificateOutputFormats=true}"
 
 SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")
 source "${SCRIPT_ROOT}/../../lib/lib.sh"
diff --git a/internal/apis/certmanager/types_certificate.go b/internal/apis/certmanager/types_certificate.go
index 7587c50b1..795ff8bf3 100644
--- a/internal/apis/certmanager/types_certificate.go
+++ b/internal/apis/certmanager/types_certificate.go
@@ -165,6 +165,11 @@ type CertificateSpec struct {
 	// revisionHistoryLimit must be a value of `1` or greater. If unset (`nil`),
 	// revisions will not be garbage collected. Default value is `nil`.
 	RevisionHistoryLimit *int32
+
+	// AdditionalOutputFormats allows for requests of additional output formats
+	// of the private key and the certificate to be written to the secret.
+	// This is an Alpha Feature and should be enabled with --feature-gates option.
+	AdditionalOutputFormats []AdditionalOutputFormat
 }
 
 // CertificatePrivateKey contains configuration options for private keys
@@ -205,6 +210,29 @@ type CertificatePrivateKey struct {
 	Size int
 }
 
+// OutputFormatType specifies which additional output formats should be added to Kubernetes secrets.
+// Allowed values are `DER` or `CombinedPEM`.
+// When Type is set to `DER` an additional entry `key.der` will be created in the secret
+// containing the binary format of the key.
+// When Type is set to `CombinedPEM` an additional entry `tls-combined.pem` will be created in the secret
+// containing the PEM formatted certificate chain and key (tls.key + tls.crt concatenated)
+type OutputFormatType string
+
+const (
+	// AdditionalOutputFormatDER requests that the DER binary format of the key
+	// is stored in the `key.der` key of a secret
+	AdditionalOutputFormatDER OutputFormatType = "DER"
+
+	// AdditionalOutputFormatCombinedPEM requests that an entry containing both the PEM tls.crt and tls.key
+	// values concatenated is stored in the `tls-combined.pem` key of a secret
+	AdditionalOutputFormatCombinedPEM OutputFormatType = "CombinedPEM"
+)
+
+// AdditionalOutputFormat wraps an additional output format type
+type AdditionalOutputFormat struct {
+	Type OutputFormatType
+}
+
 // Denotes how private keys should be generated or sourced when a Certificate
 // is being issued.
 type PrivateKeyRotationPolicy string
diff --git a/internal/apis/certmanager/v1/zz_generated.conversion.go b/internal/apis/certmanager/v1/zz_generated.conversion.go
index c98556592..41680fb7f 100644
--- a/internal/apis/certmanager/v1/zz_generated.conversion.go
+++ b/internal/apis/certmanager/v1/zz_generated.conversion.go
@@ -44,6 +44,16 @@ func init() {
 // RegisterConversions adds conversion functions to the given scheme.
 // Public to allow building arbitrary schemes.
 func RegisterConversions(s *runtime.Scheme) error {
+	if err := s.AddGeneratedConversionFunc((*v1.AdditionalOutputFormat)(nil), (*certmanager.AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(a.(*v1.AdditionalOutputFormat), b.(*certmanager.AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*certmanager.AdditionalOutputFormat)(nil), (*v1.AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_certmanager_AdditionalOutputFormat_To_v1_AdditionalOutputFormat(a.(*certmanager.AdditionalOutputFormat), b.(*v1.AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*v1.CAIssuer)(nil), (*certmanager.CAIssuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1_CAIssuer_To_certmanager_CAIssuer(a.(*v1.CAIssuer), b.(*certmanager.CAIssuer), scope)
 	}); err != nil {
@@ -377,6 +387,26 @@ func RegisterConversions(s *runtime.Scheme) error {
 	return nil
 }
 
+func autoConvert_v1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *v1.AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = certmanager.OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_v1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_v1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *v1.AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_v1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in, out, s)
+}
+
+func autoConvert_certmanager_AdditionalOutputFormat_To_v1_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *v1.AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = v1.OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_certmanager_AdditionalOutputFormat_To_v1_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_certmanager_AdditionalOutputFormat_To_v1_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *v1.AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_certmanager_AdditionalOutputFormat_To_v1_AdditionalOutputFormat(in, out, s)
+}
+
 func autoConvert_v1_CAIssuer_To_certmanager_CAIssuer(in *v1.CAIssuer, out *certmanager.CAIssuer, s conversion.Scope) error {
 	out.SecretName = in.SecretName
 	out.CRLDistributionPoints = *(*[]string)(unsafe.Pointer(&in.CRLDistributionPoints))
@@ -803,6 +833,7 @@ func autoConvert_v1_CertificateSpec_To_certmanager_CertificateSpec(in *v1.Certif
 	out.PrivateKey = (*certmanager.CertificatePrivateKey)(unsafe.Pointer(in.PrivateKey))
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]certmanager.AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
@@ -834,6 +865,7 @@ func autoConvert_certmanager_CertificateSpec_To_v1_CertificateSpec(in *certmanag
 	out.PrivateKey = (*v1.CertificatePrivateKey)(unsafe.Pointer(in.PrivateKey))
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]v1.AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
diff --git a/internal/apis/certmanager/v1alpha2/types_certificate.go b/internal/apis/certmanager/v1alpha2/types_certificate.go
index 5b3f7eafa..98c02aab6 100644
--- a/internal/apis/certmanager/v1alpha2/types_certificate.go
+++ b/internal/apis/certmanager/v1alpha2/types_certificate.go
@@ -208,6 +208,12 @@ type CertificateSpec struct {
 	// +kubebuilder:validation:ExclusiveMaximum=false
 	// +optional
 	RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` // Validated by the validating webhook.
+
+	// AdditionalOutputFormats allows for requests of additional output formats
+	// of the private key and the certificate to be written to the secret.
+	// This is an Alpha Feature and should be enabled with --feature-gates option.
+	// +optional
+	AdditionalOutputFormats []AdditionalOutputFormat `json:"additionalOutputFormats,omitempty"`
 }
 
 // CertificatePrivateKey contains configuration options for private keys
@@ -442,3 +448,27 @@ type CertificateSecretTemplate struct {
 	// +optional
 	Labels map[string]string `json:"labels,omitempty"`
 }
+
+// OutputFormatType specifies which additional output formats should be added to Kubernetes secrets.
+// Allowed values are `DER` or `CombinedPEM`.
+// When Type is set to `DER` an additional entry `key.der` will be created in the secret
+// containing the binary format of the key.
+// When Type is set to `CombinedPEM` an additional entry `tls-combined.pem` will be created in the secret
+// containing the PEM formatted certificate chain and key (tls.key + tls.crt concatenated)
+// +kubebuilder:validation:Enum=DER;CombinedPEM
+type OutputFormatType string
+
+const (
+	// AdditionalOutputFormatDER requests that the DER binary format of the key
+	// is stored in the `key.der` key of a secret
+	AdditionalOutputFormatDER OutputFormatType = "DER"
+
+	// AdditionalOutputFormatCombinedPEM requests that an entry containing both the PEM tls.crt and tls.key
+	// values concatenated is stored in the `tls-combined.pem` key of a secret
+	AdditionalOutputFormatCombinedPEM OutputFormatType = "CombinedPEM"
+)
+
+// AdditionalOutputFormat wraps an additional key output format type
+type AdditionalOutputFormat struct {
+	Type OutputFormatType `json:"type"`
+}
diff --git a/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go b/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go
index 90b7b1ff3..f2f14fdfa 100644
--- a/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go
+++ b/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go
@@ -47,6 +47,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*AdditionalOutputFormat)(nil), (*certmanager.AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha2_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(a.(*AdditionalOutputFormat), b.(*certmanager.AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*certmanager.AdditionalOutputFormat)(nil), (*AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_certmanager_AdditionalOutputFormat_To_v1alpha2_AdditionalOutputFormat(a.(*certmanager.AdditionalOutputFormat), b.(*AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*certmanager.CAIssuer)(nil), (*CAIssuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_certmanager_CAIssuer_To_v1alpha2_CAIssuer(a.(*certmanager.CAIssuer), b.(*CAIssuer), scope)
 	}); err != nil {
@@ -375,6 +385,26 @@ func RegisterConversions(s *runtime.Scheme) error {
 	return nil
 }
 
+func autoConvert_v1alpha2_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = certmanager.OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_v1alpha2_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_v1alpha2_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_v1alpha2_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in, out, s)
+}
+
+func autoConvert_certmanager_AdditionalOutputFormat_To_v1alpha2_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_certmanager_AdditionalOutputFormat_To_v1alpha2_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_certmanager_AdditionalOutputFormat_To_v1alpha2_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_certmanager_AdditionalOutputFormat_To_v1alpha2_AdditionalOutputFormat(in, out, s)
+}
+
 func autoConvert_v1alpha2_CAIssuer_To_certmanager_CAIssuer(in *CAIssuer, out *certmanager.CAIssuer, s conversion.Scope) error {
 	out.SecretName = in.SecretName
 	out.CRLDistributionPoints = *(*[]string)(unsafe.Pointer(&in.CRLDistributionPoints))
@@ -803,6 +833,7 @@ func autoConvert_v1alpha2_CertificateSpec_To_certmanager_CertificateSpec(in *Cer
 	}
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]certmanager.AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
@@ -850,6 +881,7 @@ func autoConvert_certmanager_CertificateSpec_To_v1alpha2_CertificateSpec(in *cer
 	}
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
diff --git a/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go b/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go
index 157377d8b..6033dc64e 100644
--- a/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go
+++ b/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go
@@ -28,6 +28,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdditionalOutputFormat) DeepCopyInto(out *AdditionalOutputFormat) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalOutputFormat.
+func (in *AdditionalOutputFormat) DeepCopy() *AdditionalOutputFormat {
+	if in == nil {
+		return nil
+	}
+	out := new(AdditionalOutputFormat)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CAIssuer) DeepCopyInto(out *CAIssuer) {
 	*out = *in
@@ -451,6 +467,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
 		*out = new(int32)
 		**out = **in
 	}
+	if in.AdditionalOutputFormats != nil {
+		in, out := &in.AdditionalOutputFormats, &out.AdditionalOutputFormats
+		*out = make([]AdditionalOutputFormat, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
diff --git a/internal/apis/certmanager/v1alpha3/types_certificate.go b/internal/apis/certmanager/v1alpha3/types_certificate.go
index 7ae1ea420..6da185f6c 100644
--- a/internal/apis/certmanager/v1alpha3/types_certificate.go
+++ b/internal/apis/certmanager/v1alpha3/types_certificate.go
@@ -206,6 +206,12 @@ type CertificateSpec struct {
 	// +kubebuilder:validation:ExclusiveMaximum=false
 	// +optional
 	RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` // Validated by the validating webhook.
+
+	// AdditionalOutputFormats allows for requests of additional output formats
+	// of the private key and the certificate to be written to the secret.
+	// This is an Alpha Feature and should be enabled with --feature-gates option.
+	// +optional
+	AdditionalOutputFormats []AdditionalOutputFormat `json:"additionalOutputFormats,omitempty"`
 }
 
 // CertificatePrivateKey contains configuration options for private keys
@@ -449,3 +455,27 @@ type CertificateSecretTemplate struct {
 	// +optional
 	Labels map[string]string `json:"labels,omitempty"`
 }
+
+// OutputFormatType specifies which additional output formats should be added to Kubernetes secrets.
+// Allowed values are `DER` or `CombinedPEM`.
+// When Type is set to `DER` an additional entry `key.der` will be created in the secret
+// containing the binary format of the key.
+// When Type is set to `CombinedPEM` an additional entry `tls-combined.pem` will be created in the secret
+// containing the PEM formatted certificate chain and key (tls.key + tls.crt concatenated)
+// +kubebuilder:validation:Enum=DER;CombinedPEM
+type OutputFormatType string
+
+const (
+	// AdditionalOutputFormatDER requests that the DER binary format of the key
+	// is stored in the `key.der` key of a secret
+	AdditionalOutputFormatDER OutputFormatType = "DER"
+
+	// AdditionalOutputFormatCombinedPEM requests that an entry containing both the PEM tls.crt and tls.key
+	// values concatenated is stored in the `tls-combined.pem` key of a secret
+	AdditionalOutputFormatCombinedPEM OutputFormatType = "CombinedPEM"
+)
+
+// AdditionalOutputFormat wraps an additional key output format type
+type AdditionalOutputFormat struct {
+	Type OutputFormatType `json:"type"`
+}
diff --git a/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go b/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go
index 63133fcb4..d762ca703 100644
--- a/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go
+++ b/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go
@@ -42,6 +42,16 @@ func init() {
 // RegisterConversions adds conversion functions to the given scheme.
 // Public to allow building arbitrary schemes.
 func RegisterConversions(s *runtime.Scheme) error {
+	if err := s.AddGeneratedConversionFunc((*AdditionalOutputFormat)(nil), (*certmanager.AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha3_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(a.(*AdditionalOutputFormat), b.(*certmanager.AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*certmanager.AdditionalOutputFormat)(nil), (*AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_certmanager_AdditionalOutputFormat_To_v1alpha3_AdditionalOutputFormat(a.(*certmanager.AdditionalOutputFormat), b.(*AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*CAIssuer)(nil), (*certmanager.CAIssuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_CAIssuer_To_certmanager_CAIssuer(a.(*CAIssuer), b.(*certmanager.CAIssuer), scope)
 	}); err != nil {
@@ -375,6 +385,26 @@ func RegisterConversions(s *runtime.Scheme) error {
 	return nil
 }
 
+func autoConvert_v1alpha3_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = certmanager.OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_v1alpha3_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_v1alpha3_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_v1alpha3_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in, out, s)
+}
+
+func autoConvert_certmanager_AdditionalOutputFormat_To_v1alpha3_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_certmanager_AdditionalOutputFormat_To_v1alpha3_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_certmanager_AdditionalOutputFormat_To_v1alpha3_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_certmanager_AdditionalOutputFormat_To_v1alpha3_AdditionalOutputFormat(in, out, s)
+}
+
 func autoConvert_v1alpha3_CAIssuer_To_certmanager_CAIssuer(in *CAIssuer, out *certmanager.CAIssuer, s conversion.Scope) error {
 	out.SecretName = in.SecretName
 	out.CRLDistributionPoints = *(*[]string)(unsafe.Pointer(&in.CRLDistributionPoints))
@@ -802,6 +832,7 @@ func autoConvert_v1alpha3_CertificateSpec_To_certmanager_CertificateSpec(in *Cer
 	}
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]certmanager.AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
@@ -849,6 +880,7 @@ func autoConvert_certmanager_CertificateSpec_To_v1alpha3_CertificateSpec(in *cer
 	}
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
diff --git a/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go b/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go
index 341f44cf0..40757a98d 100644
--- a/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go
+++ b/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go
@@ -28,6 +28,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdditionalOutputFormat) DeepCopyInto(out *AdditionalOutputFormat) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalOutputFormat.
+func (in *AdditionalOutputFormat) DeepCopy() *AdditionalOutputFormat {
+	if in == nil {
+		return nil
+	}
+	out := new(AdditionalOutputFormat)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CAIssuer) DeepCopyInto(out *CAIssuer) {
 	*out = *in
@@ -446,6 +462,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
 		*out = new(int32)
 		**out = **in
 	}
+	if in.AdditionalOutputFormats != nil {
+		in, out := &in.AdditionalOutputFormats, &out.AdditionalOutputFormats
+		*out = make([]AdditionalOutputFormat, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
diff --git a/internal/apis/certmanager/v1beta1/types_certificate.go b/internal/apis/certmanager/v1beta1/types_certificate.go
index 50a6d2f8f..19c319131 100644
--- a/internal/apis/certmanager/v1beta1/types_certificate.go
+++ b/internal/apis/certmanager/v1beta1/types_certificate.go
@@ -183,6 +183,12 @@ type CertificateSpec struct {
 	// +kubebuilder:validation:ExclusiveMaximum=false
 	// +optional
 	RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` // Validated by the validating webhook.
+
+	// AdditionalOutputFormats allows for requests of additional output formats
+	// of the private key and the certificate to be written to the secret.
+	// This is an Alpha Feature and should be enabled with --feature-gates option.
+	// +optional
+	AdditionalOutputFormats []AdditionalOutputFormat `json:"additionalOutputFormats,omitempty"`
 }
 
 // CertificatePrivateKey contains configuration options for private keys
@@ -447,3 +453,27 @@ type CertificateSecretTemplate struct {
 	// +optional
 	Labels map[string]string `json:"labels,omitempty"`
 }
+
+// OutputFormatType specifies which additional output formats should be added to Kubernetes secrets.
+// Allowed values are `DER` or `CombinedPEM`.
+// When Type is set to `DER` an additional entry `key.der` will be created in the secret
+// containing the binary format of the key.
+// When Type is set to `CombinedPEM` an additional entry `tls-combined.pem` will be created in the secret
+// containing the PEM formatted certificate chain and key (tls.key + tls.crt concatenated)
+// +kubebuilder:validation:Enum=DER;CombinedPEM
+type OutputFormatType string
+
+const (
+	// AdditionalOutputFormatDER requests that the DER binary format of the key
+	// is stored in the `key.der` key of a secret
+	AdditionalOutputFormatDER OutputFormatType = "DER"
+
+	// AdditionalOutputFormatCombinedPEM requests that an entry containing both the PEM tls.crt and tls.key
+	// values concatenated is stored in the `tls-combined.pem` key of a secret
+	AdditionalOutputFormatCombinedPEM OutputFormatType = "CombinedPEM"
+)
+
+// AdditionalOutputFormat wraps an additional key output format type
+type AdditionalOutputFormat struct {
+	Type OutputFormatType `json:"type"`
+}
diff --git a/internal/apis/certmanager/v1beta1/zz_generated.conversion.go b/internal/apis/certmanager/v1beta1/zz_generated.conversion.go
index e17b9bf21..d8f1de958 100644
--- a/internal/apis/certmanager/v1beta1/zz_generated.conversion.go
+++ b/internal/apis/certmanager/v1beta1/zz_generated.conversion.go
@@ -42,6 +42,16 @@ func init() {
 // RegisterConversions adds conversion functions to the given scheme.
 // Public to allow building arbitrary schemes.
 func RegisterConversions(s *runtime.Scheme) error {
+	if err := s.AddGeneratedConversionFunc((*AdditionalOutputFormat)(nil), (*certmanager.AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(a.(*AdditionalOutputFormat), b.(*certmanager.AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*certmanager.AdditionalOutputFormat)(nil), (*AdditionalOutputFormat)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_certmanager_AdditionalOutputFormat_To_v1beta1_AdditionalOutputFormat(a.(*certmanager.AdditionalOutputFormat), b.(*AdditionalOutputFormat), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*CAIssuer)(nil), (*certmanager.CAIssuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_CAIssuer_To_certmanager_CAIssuer(a.(*CAIssuer), b.(*certmanager.CAIssuer), scope)
 	}); err != nil {
@@ -372,9 +382,34 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*certmanager.CertificatePrivateKey)(nil), (*CertificatePrivateKey)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_certmanager_CertificatePrivateKey_To_v1beta1_CertificatePrivateKey(a.(*certmanager.CertificatePrivateKey), b.(*CertificatePrivateKey), scope)
+	}); err != nil {
+		return err
+	}
+	return nil
+}
+
+func autoConvert_v1beta1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = certmanager.OutputFormatType(in.Type)
+	return nil
+}
+
+// Convert_v1beta1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_v1beta1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in *AdditionalOutputFormat, out *certmanager.AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_v1beta1_AdditionalOutputFormat_To_certmanager_AdditionalOutputFormat(in, out, s)
+}
+
+func autoConvert_certmanager_AdditionalOutputFormat_To_v1beta1_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *AdditionalOutputFormat, s conversion.Scope) error {
+	out.Type = OutputFormatType(in.Type)
 	return nil
 }
 
+// Convert_certmanager_AdditionalOutputFormat_To_v1beta1_AdditionalOutputFormat is an autogenerated conversion function.
+func Convert_certmanager_AdditionalOutputFormat_To_v1beta1_AdditionalOutputFormat(in *certmanager.AdditionalOutputFormat, out *AdditionalOutputFormat, s conversion.Scope) error {
+	return autoConvert_certmanager_AdditionalOutputFormat_To_v1beta1_AdditionalOutputFormat(in, out, s)
+}
+
 func autoConvert_v1beta1_CAIssuer_To_certmanager_CAIssuer(in *CAIssuer, out *certmanager.CAIssuer, s conversion.Scope) error {
 	out.SecretName = in.SecretName
 	out.CRLDistributionPoints = *(*[]string)(unsafe.Pointer(&in.CRLDistributionPoints))
@@ -798,9 +833,18 @@ func autoConvert_v1beta1_CertificateSpec_To_certmanager_CertificateSpec(in *Cert
 	}
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]certmanager.KeyUsage)(unsafe.Pointer(&in.Usages))
-	out.PrivateKey = (*certmanager.CertificatePrivateKey)(unsafe.Pointer(in.PrivateKey))
+	if in.PrivateKey != nil {
+		in, out := &in.PrivateKey, &out.PrivateKey
+		*out = new(certmanager.CertificatePrivateKey)
+		if err := Convert_v1beta1_CertificatePrivateKey_To_certmanager_CertificatePrivateKey(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.PrivateKey = nil
+	}
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]certmanager.AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
@@ -837,6 +881,7 @@ func autoConvert_certmanager_CertificateSpec_To_v1beta1_CertificateSpec(in *cert
 	out.PrivateKey = (*CertificatePrivateKey)(unsafe.Pointer(in.PrivateKey))
 	out.EncodeUsagesInRequest = (*bool)(unsafe.Pointer(in.EncodeUsagesInRequest))
 	out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
+	out.AdditionalOutputFormats = *(*[]AdditionalOutputFormat)(unsafe.Pointer(&in.AdditionalOutputFormats))
 	return nil
 }
 
diff --git a/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go b/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go
index eb7a61fc4..5db1b2810 100644
--- a/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go
+++ b/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go
@@ -28,6 +28,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdditionalOutputFormat) DeepCopyInto(out *AdditionalOutputFormat) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalOutputFormat.
+func (in *AdditionalOutputFormat) DeepCopy() *AdditionalOutputFormat {
+	if in == nil {
+		return nil
+	}
+	out := new(AdditionalOutputFormat)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CAIssuer) DeepCopyInto(out *CAIssuer) {
 	*out = *in
@@ -446,6 +462,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
 		*out = new(int32)
 		**out = **in
 	}
+	if in.AdditionalOutputFormats != nil {
+		in, out := &in.AdditionalOutputFormats, &out.AdditionalOutputFormats
+		*out = make([]AdditionalOutputFormat, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
diff --git a/internal/apis/certmanager/validation/BUILD.bazel b/internal/apis/certmanager/validation/BUILD.bazel
index e4a020d54..abf9b4093 100644
--- a/internal/apis/certmanager/validation/BUILD.bazel
+++ b/internal/apis/certmanager/validation/BUILD.bazel
@@ -23,11 +23,13 @@ go_library(
         "//internal/apis/certmanager/v1beta1:go_default_library",
         "//internal/apis/certmanager/validation/util:go_default_library",
         "//internal/apis/meta:go_default_library",
+        "//internal/controller/feature:go_default_library",
         "//pkg/api/util:go_default_library",
         "//pkg/apis/acme:go_default_library",
         "//pkg/apis/certmanager:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/util:go_default_library",
+        "//pkg/util/feature:go_default_library",
         "//pkg/util/pki:go_default_library",
         "@com_github_kr_pretty//:go_default_library",
         "@io_k8s_api//admission/v1:go_default_library",
diff --git a/internal/apis/certmanager/validation/certificate.go b/internal/apis/certmanager/validation/certificate.go
index 69e969703..c6c46c68f 100644
--- a/internal/apis/certmanager/validation/certificate.go
+++ b/internal/apis/certmanager/validation/certificate.go
@@ -31,8 +31,10 @@ import (
 	"github.com/jetstack/cert-manager/internal/api/validation"
 	internalcmapi "github.com/jetstack/cert-manager/internal/apis/certmanager"
 	cmmeta "github.com/jetstack/cert-manager/internal/apis/meta"
+	"github.com/jetstack/cert-manager/internal/controller/feature"
 	"github.com/jetstack/cert-manager/pkg/api/util"
 	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	utilfeature "github.com/jetstack/cert-manager/pkg/util/feature"
 )
 
 // Validation functions for cert-manager Certificate types
@@ -77,6 +79,19 @@ func ValidateCertificateSpec(crt *internalcmapi.CertificateSpec, fldPath *field.
 		default:
 			el = append(el, field.Invalid(fldPath.Child("privateKey", "algorithm"), crt.PrivateKey.Algorithm, "must be either empty or one of rsa or ecdsa"))
 		}
+		if crt.AdditionalOutputFormats != nil {
+			if !utilfeature.DefaultFeatureGate.Enabled(feature.AdditionalCertificateOutputFormats) {
+				el = append(el, field.Forbidden(fldPath.Child("AdditionalOutputFormat"), "Feature gate AdditionalCertificateOutputFormats must be enabled"))
+			}
+			check := make(map[string]bool)
+			for _, val := range crt.AdditionalOutputFormats {
+				if _, exists := check[string(val.Type)]; !exists {
+					check[string(val.Type)] = true
+				} else {
+					el = append(el, field.Invalid(fldPath.Child("AdditionalOutputFormats"), crt.AdditionalOutputFormats, "Duplicate Type in additionalOutputFormats"))
+				}
+			}
+		}
 	}
 
 	if crt.Duration != nil || crt.RenewBefore != nil {
diff --git a/internal/apis/certmanager/zz_generated.deepcopy.go b/internal/apis/certmanager/zz_generated.deepcopy.go
index e52b1b6b2..520afe13e 100644
--- a/internal/apis/certmanager/zz_generated.deepcopy.go
+++ b/internal/apis/certmanager/zz_generated.deepcopy.go
@@ -28,6 +28,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdditionalOutputFormat) DeepCopyInto(out *AdditionalOutputFormat) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalOutputFormat.
+func (in *AdditionalOutputFormat) DeepCopy() *AdditionalOutputFormat {
+	if in == nil {
+		return nil
+	}
+	out := new(AdditionalOutputFormat)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CAIssuer) DeepCopyInto(out *CAIssuer) {
 	*out = *in
@@ -446,6 +462,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
 		*out = new(int32)
 		**out = **in
 	}
+	if in.AdditionalOutputFormats != nil {
+		in, out := &in.AdditionalOutputFormats, &out.AdditionalOutputFormats
+		*out = make([]AdditionalOutputFormat, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
diff --git a/internal/controller/feature/features.go b/internal/controller/feature/features.go
index 07274ab4e..c614d53b0 100644
--- a/internal/controller/feature/features.go
+++ b/internal/controller/feature/features.go
@@ -40,6 +40,11 @@ const (
 	// ExperimentalGatewayAPISupport enables the gateway-shim controller and adds support for
 	// the Gateway API to the HTTP-01 challenge solver.
 	ExperimentalGatewayAPISupport featuregate.Feature = "ExperimentalGatewayAPISupport"
+
+	// alpha: v1.7.0
+	//
+	// AdditionalCertificateOutputFormats enable output additional format
+	AdditionalCertificateOutputFormats featuregate.Feature = "AdditionalCertificateOutputFormats"
 )
 
 func init() {
@@ -53,4 +58,5 @@ var defaultCertManagerFeatureGates = map[featuregate.Feature]featuregate.Feature
 	ValidateCAA: {Default: false, PreRelease: featuregate.Alpha},
 	ExperimentalCertificateSigningRequestControllers: {Default: false, PreRelease: featuregate.Alpha},
 	ExperimentalGatewayAPISupport:                    {Default: false, PreRelease: featuregate.Alpha},
+	AdditionalCertificateOutputFormats:               {Default: false, PreRelease: featuregate.Alpha},
 }
diff --git a/pkg/apis/certmanager/v1/types_certificate.go b/pkg/apis/certmanager/v1/types_certificate.go
index 35860347a..cbf8ba003 100644
--- a/pkg/apis/certmanager/v1/types_certificate.go
+++ b/pkg/apis/certmanager/v1/types_certificate.go
@@ -187,6 +187,12 @@ type CertificateSpec struct {
 	// +kubebuilder:validation:ExclusiveMaximum=false
 	// +optional
 	RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` // Validated by the validating webhook.
+
+	// AdditionalOutputFormats allows for requests of additional output formats
+	// of the private key and the certificate to be written to the secret.
+	// This is an Alpha Feature and should be enabled with --feature-gates option.
+	// +optional
+	AdditionalOutputFormats []AdditionalOutputFormat `json:"additionalOutputFormats,omitempty"`
 }
 
 // CertificatePrivateKey contains configuration options for private keys
@@ -249,6 +255,38 @@ var (
 	RotationPolicyAlways PrivateKeyRotationPolicy = "Always"
 )
 
+// OutputFormatType specifies which additional output formats should be added to Kubernetes secrets.
+// Allowed values are `DER` or `CombinedPEM`.
+// When Type is set to `DER` an additional entry `key.der` will be created in the secret
+// containing the binary format of the key.
+// When Type is set to `CombinedPEM` an additional entry `tls-combined.pem` will be created in the secret
+// containing the PEM formatted certificate chain and key (tls.key + tls.crt concatenated)
+// +kubebuilder:validation:Enum=DER;CombinedPEM
+type OutputFormatType string
+
+const (
+	// AdditionalOutputFormatDER requests that the DER binary format of the key
+	// is stored in the `key.der` key of the certificate's secret.
+	AdditionalOutputFormatDER OutputFormatType = "DER"
+
+	// AdditionalOutputFormatDERKey is the name of the data entry in the Secret resource
+	// used to store the DER formatted private key.
+	AdditionalOutputFormatDERKey string = "key.der"
+
+	// AdditionalOutputFormatCombinedPEM requests that an entry containing both the PEM tls.crt and tls.key
+	// values concatenated is stored in the `tls-combined.pem` key of the certificate's secret.
+	AdditionalOutputFormatCombinedPEM OutputFormatType = "CombinedPEM"
+
+	// AdditionalOutputFormatPEMKey is the name of the data entry in the Secret resource
+	// used to store the combined PEM certificate + key.
+	AdditionalOutputFormatPEMKey string = "tls-combined.pem"
+)
+
+// AdditionalOutputFormat wraps an additional output format type
+type AdditionalOutputFormat struct {
+	Type OutputFormatType `json:"type"`
+}
+
 // X509Subject Full X509 name specification
 type X509Subject struct {
 	// Organizations to be used on the Certificate.
diff --git a/pkg/apis/certmanager/v1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1/zz_generated.deepcopy.go
index ba39e0858..2dc0428bc 100644
--- a/pkg/apis/certmanager/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/certmanager/v1/zz_generated.deepcopy.go
@@ -28,6 +28,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdditionalOutputFormat) DeepCopyInto(out *AdditionalOutputFormat) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalOutputFormat.
+func (in *AdditionalOutputFormat) DeepCopy() *AdditionalOutputFormat {
+	if in == nil {
+		return nil
+	}
+	out := new(AdditionalOutputFormat)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CAIssuer) DeepCopyInto(out *CAIssuer) {
 	*out = *in
@@ -446,6 +462,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
 		*out = new(int32)
 		**out = **in
 	}
+	if in.AdditionalOutputFormats != nil {
+		in, out := &in.AdditionalOutputFormats, &out.AdditionalOutputFormats
+		*out = make([]AdditionalOutputFormat, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
diff --git a/pkg/controller/certificates/internal/secretsmanager/BUILD.bazel b/pkg/controller/certificates/internal/secretsmanager/BUILD.bazel
index 0c3b77628..df5f1e066 100644
--- a/pkg/controller/certificates/internal/secretsmanager/BUILD.bazel
+++ b/pkg/controller/certificates/internal/secretsmanager/BUILD.bazel
@@ -9,9 +9,11 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/controller/certificates/internal/secretsmanager",
     visibility = ["//pkg/controller/certificates:__subpackages__"],
     deps = [
+        "//internal/controller/feature:go_default_library",
         "//pkg/api/util:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/apis/meta/v1:go_default_library",
+        "//pkg/util/feature:go_default_library",
         "//pkg/util/pki:go_default_library",
         "@com_github_pavel_v_chernykh_keystore_go_v4//:go_default_library",
         "@com_sslmate_software_src_go_pkcs12//:go_default_library",
@@ -31,11 +33,13 @@ go_test(
     ],
     embed = [":go_default_library"],
     deps = [
+        "//internal/controller/feature:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/apis/meta/v1:go_default_library",
         "//pkg/controller:go_default_library",
         "//pkg/controller/certificates/internal/test:go_default_library",
         "//pkg/controller/test:go_default_library",
+        "//pkg/util/feature:go_default_library",
         "//pkg/util/pki:go_default_library",
         "//test/unit/gen:go_default_library",
         "@com_github_google_gofuzz//:go_default_library",
@@ -47,6 +51,7 @@ go_test(
         "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
         "@io_k8s_client_go//testing:go_default_library",
+        "@io_k8s_component_base//featuregate/testing:go_default_library",
         "@io_k8s_utils//clock/testing:go_default_library",
         "@org_golang_x_sync//errgroup:go_default_library",
         "@org_golang_x_sync//semaphore:go_default_library",
diff --git a/pkg/controller/certificates/internal/secretsmanager/secret.go b/pkg/controller/certificates/internal/secretsmanager/secret.go
index e424a26d9..11b9e5f9d 100644
--- a/pkg/controller/certificates/internal/secretsmanager/secret.go
+++ b/pkg/controller/certificates/internal/secretsmanager/secret.go
@@ -19,6 +19,7 @@ package secretsmanager
 import (
 	"bytes"
 	"context"
+	"encoding/pem"
 	"fmt"
 	"strings"
 
@@ -28,9 +29,11 @@ import (
 	"k8s.io/client-go/kubernetes"
 	corelisters "k8s.io/client-go/listers/core/v1"
 
+	"github.com/jetstack/cert-manager/internal/controller/feature"
 	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
 	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
+	utilfeature "github.com/jetstack/cert-manager/pkg/util/feature"
 	utilpki "github.com/jetstack/cert-manager/pkg/util/pki"
 )
 
@@ -120,6 +123,45 @@ func (s *SecretsManager) UpdateData(ctx context.Context, crt *cmapi.Certificate,
 	return err
 }
 
+func updateSecretWithAdditionalOutputFormats(crt *cmapi.Certificate, secret *corev1.Secret, data SecretData) error {
+	if crt.Spec.AdditionalOutputFormats == nil {
+		delete(secret.Data, cmapi.AdditionalOutputFormatDERKey)
+		delete(secret.Data, cmapi.AdditionalOutputFormatPEMKey)
+		return nil
+	}
+
+	additionalOutputFormatDER := false
+	additionalOutputFormatPEM := false
+
+	for _, f := range crt.Spec.AdditionalOutputFormats {
+		switch f.Type {
+		case cmapi.AdditionalOutputFormatDER:
+			additionalOutputFormatDER = true
+		case cmapi.AdditionalOutputFormatCombinedPEM:
+			additionalOutputFormatPEM = true
+		default:
+			return fmt.Errorf("unknown additional output format %s", f.Type)
+		}
+	}
+
+	if additionalOutputFormatDER {
+		// Store binary format of the private key
+		block, _ := pem.Decode(data.PrivateKey)
+		secret.Data[cmapi.AdditionalOutputFormatDERKey] = block.Bytes
+	} else {
+		delete(secret.Data, cmapi.AdditionalOutputFormatDERKey)
+	}
+
+	if additionalOutputFormatPEM {
+		// Combine tls.key and tls.crt
+		secret.Data[cmapi.AdditionalOutputFormatPEMKey] = []byte(strings.Join([]string{string(data.PrivateKey), string(data.Certificate)}, "\n"))
+	} else {
+		delete(secret.Data, cmapi.AdditionalOutputFormatPEMKey)
+	}
+
+	return nil
+}
+
 // setValues will update the Secret resource 'secret' with the data contained
 // in the given secretData.
 // It will update labels and annotations on the Secret resource appropriately.
@@ -203,8 +245,14 @@ func (s *SecretsManager) setValues(crt *cmapi.Certificate, secret *corev1.Secret
 			delete(secret.Data, jksSecretKey)
 			delete(secret.Data, jksTruststoreKey)
 		}
-	}
 
+		// Add additional output formats
+		if utilfeature.DefaultFeatureGate.Enabled(feature.AdditionalCertificateOutputFormats) {
+			if err := updateSecretWithAdditionalOutputFormats(crt, secret, data); err != nil {
+				return fmt.Errorf("error during additional output format update: %w", err)
+			}
+		}
+	}
 	secret.Data[corev1.TLSPrivateKeyKey] = data.PrivateKey
 	secret.Data[corev1.TLSCertKey] = data.Certificate
 	if len(data.CA) > 0 {


Test patch:
diff --git a/pkg/controller/certificates/internal/secretsmanager/secret_test.go b/pkg/controller/certificates/internal/secretsmanager/secret_test.go
index 223ff0d7b..62ee9ae96 100644
--- a/pkg/controller/certificates/internal/secretsmanager/secret_test.go
+++ b/pkg/controller/certificates/internal/secretsmanager/secret_test.go
@@ -18,14 +18,18 @@ package secretsmanager
 
 import (
 	"context"
+	"encoding/pem"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/jetstack/cert-manager/internal/controller/feature"
+	utilfeature "github.com/jetstack/cert-manager/pkg/util/feature"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	coretesting "k8s.io/client-go/testing"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
 	fakeclock "k8s.io/utils/clock/testing"
 
 	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
@@ -71,6 +75,22 @@ func TestSecretsManager(t *testing.T) {
 			"template": "label",
 		}),
 	)
+	// enable feature gate additional private key for this test
+	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultMutableFeatureGate, feature.AdditionalCertificateOutputFormats, true)()
+	baseCertWithAdditionalOutputFormatDER := gen.CertificateFrom(baseCertBundle.Certificate,
+		gen.SetCertificateAdditionalOutputFormats(cmapi.AdditionalOutputFormat{Type: "DER"}),
+	)
+	baseCertWithAdditionalOutputFormatCombinedPEM := gen.CertificateFrom(baseCertBundle.Certificate,
+		gen.SetCertificateAdditionalOutputFormats(cmapi.AdditionalOutputFormat{Type: "CombinedPEM"}),
+	)
+	baseCertWithAdditionalOutputFormats := gen.CertificateFrom(baseCertBundle.Certificate,
+		gen.SetCertificateAdditionalOutputFormats(
+			cmapi.AdditionalOutputFormat{Type: "DER"},
+			cmapi.AdditionalOutputFormat{Type: "CombinedPEM"},
+		),
+	)
+	block, _ := pem.Decode(baseCertBundle.PrivateKeyBytes)
+	tlsDerContent := block.Bytes
 
 	tests := map[string]testT{
 		"if secret does not exists and unable to decode certificate, then error": {
@@ -400,6 +420,315 @@ func TestSecretsManager(t *testing.T) {
 			},
 			expectedErr: false,
 		},
+
+		"if secret does not exist, create new Secret with additional output format DER": {
+			certificate: baseCertWithAdditionalOutputFormatDER,
+			SecretData:  SecretData{Certificate: baseCertBundle.CertBytes, CA: []byte("test-ca"), PrivateKey: baseCertBundle.PrivateKeyBytes},
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						corev1.SchemeGroupVersion.WithResource("secrets"),
+						gen.DefaultTestNamespace,
+						&corev1.Secret{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: gen.DefaultTestNamespace,
+								Name:      "output",
+								Annotations: map[string]string{
+									cmapi.CertificateNameKey:       "test",
+									cmapi.IssuerGroupAnnotationKey: "foo.io",
+									cmapi.IssuerKindAnnotationKey:  "Issuer",
+									cmapi.IssuerNameAnnotationKey:  "ca-issuer",
+
+									cmapi.CommonNameAnnotationKey: baseCertBundle.Cert.Subject.CommonName,
+									cmapi.AltNamesAnnotationKey:   strings.Join(baseCertBundle.Cert.DNSNames, ","),
+									cmapi.IPSANAnnotationKey:      strings.Join(utilpki.IPAddressesToString(baseCertBundle.Cert.IPAddresses), ","),
+									cmapi.URISANAnnotationKey:     strings.Join(utilpki.URLsToString(baseCertBundle.Cert.URIs), ","),
+								},
+								Labels: map[string]string{},
+							},
+							Data: map[string][]byte{
+								corev1.TLSCertKey:                  baseCertBundle.CertBytes,
+								corev1.TLSPrivateKeyKey:            baseCertBundle.PrivateKeyBytes,
+								cmmeta.TLSCAKey:                    []byte("test-ca"),
+								cmapi.AdditionalOutputFormatDERKey: tlsDerContent,
+							},
+							Type: corev1.SecretTypeTLS,
+						},
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+
+		"if secret does not exist, create new Secret with additional output format CombinedPEM": {
+			certificate: baseCertWithAdditionalOutputFormatCombinedPEM,
+			SecretData:  SecretData{Certificate: baseCertBundle.CertBytes, CA: []byte("test-ca"), PrivateKey: baseCertBundle.PrivateKeyBytes},
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						corev1.SchemeGroupVersion.WithResource("secrets"),
+						gen.DefaultTestNamespace,
+						&corev1.Secret{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: gen.DefaultTestNamespace,
+								Name:      "output",
+								Annotations: map[string]string{
+									cmapi.CertificateNameKey:       "test",
+									cmapi.IssuerGroupAnnotationKey: "foo.io",
+									cmapi.IssuerKindAnnotationKey:  "Issuer",
+									cmapi.IssuerNameAnnotationKey:  "ca-issuer",
+
+									cmapi.CommonNameAnnotationKey: baseCertBundle.Cert.Subject.CommonName,
+									cmapi.AltNamesAnnotationKey:   strings.Join(baseCertBundle.Cert.DNSNames, ","),
+									cmapi.IPSANAnnotationKey:      strings.Join(utilpki.IPAddressesToString(baseCertBundle.Cert.IPAddresses), ","),
+									cmapi.URISANAnnotationKey:     strings.Join(utilpki.URLsToString(baseCertBundle.Cert.URIs), ","),
+								},
+								Labels: map[string]string{},
+							},
+							Data: map[string][]byte{
+								corev1.TLSCertKey:                  baseCertBundle.CertBytes,
+								corev1.TLSPrivateKeyKey:            baseCertBundle.PrivateKeyBytes,
+								cmmeta.TLSCAKey:                    []byte("test-ca"),
+								cmapi.AdditionalOutputFormatPEMKey: []byte(strings.Join([]string{string(baseCertBundle.PrivateKeyBytes), string(baseCertBundle.CertBytes)}, "\n")),
+							},
+							Type: corev1.SecretTypeTLS,
+						},
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+
+		"if secret does not exist, create new Secret with additional output format DER and CombinedPEM": {
+			certificate: baseCertWithAdditionalOutputFormats,
+			SecretData:  SecretData{Certificate: baseCertBundle.CertBytes, CA: []byte("test-ca"), PrivateKey: baseCertBundle.PrivateKeyBytes},
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						corev1.SchemeGroupVersion.WithResource("secrets"),
+						gen.DefaultTestNamespace,
+						&corev1.Secret{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: gen.DefaultTestNamespace,
+								Name:      "output",
+								Annotations: map[string]string{
+									cmapi.CertificateNameKey:       "test",
+									cmapi.IssuerGroupAnnotationKey: "foo.io",
+									cmapi.IssuerKindAnnotationKey:  "Issuer",
+									cmapi.IssuerNameAnnotationKey:  "ca-issuer",
+
+									cmapi.CommonNameAnnotationKey: baseCertBundle.Cert.Subject.CommonName,
+									cmapi.AltNamesAnnotationKey:   strings.Join(baseCertBundle.Cert.DNSNames, ","),
+									cmapi.IPSANAnnotationKey:      strings.Join(utilpki.IPAddressesToString(baseCertBundle.Cert.IPAddresses), ","),
+									cmapi.URISANAnnotationKey:     strings.Join(utilpki.URLsToString(baseCertBundle.Cert.URIs), ","),
+								},
+								Labels: map[string]string{},
+							},
+							Data: map[string][]byte{
+								corev1.TLSCertKey:                  baseCertBundle.CertBytes,
+								corev1.TLSPrivateKeyKey:            baseCertBundle.PrivateKeyBytes,
+								cmmeta.TLSCAKey:                    []byte("test-ca"),
+								cmapi.AdditionalOutputFormatDERKey: tlsDerContent,
+								cmapi.AdditionalOutputFormatPEMKey: []byte(strings.Join([]string{string(baseCertBundle.PrivateKeyBytes), string(baseCertBundle.CertBytes)}, "\n")),
+							},
+							Type: corev1.SecretTypeTLS,
+						},
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+
+		"if secret exists, with tls-combined.pem and key.der but no additional formats specified": {
+			certificate: baseCertBundle.Certificate,
+			certificateOptions: controllerpkg.CertificateOptions{
+				EnableOwnerRef: false,
+			},
+			SecretData: SecretData{Certificate: baseCertBundle.CertBytes, CA: []byte("test-ca"), PrivateKey: []byte("test-key")},
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{
+					&corev1.Secret{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: gen.DefaultTestNamespace,
+							Name:      "output",
+							Annotations: map[string]string{
+								"my-custom": "annotation",
+							},
+						},
+						Data: map[string][]byte{
+							corev1.TLSCertKey:                  []byte("foo"),
+							corev1.TLSPrivateKeyKey:            []byte("foo"),
+							cmmeta.TLSCAKey:                    []byte("foo"),
+							cmapi.AdditionalOutputFormatDERKey: []byte("foo"),
+							cmapi.AdditionalOutputFormatPEMKey: []byte("foo"),
+						},
+						Type: corev1.SecretTypeTLS,
+					},
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewUpdateAction(
+						corev1.SchemeGroupVersion.WithResource("secrets"),
+						gen.DefaultTestNamespace,
+						&corev1.Secret{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: gen.DefaultTestNamespace,
+								Name:      "output",
+								Annotations: map[string]string{
+									"my-custom": "annotation",
+
+									cmapi.CertificateNameKey:       "test",
+									cmapi.IssuerGroupAnnotationKey: "foo.io",
+									cmapi.IssuerKindAnnotationKey:  "Issuer",
+									cmapi.IssuerNameAnnotationKey:  "ca-issuer",
+
+									cmapi.CommonNameAnnotationKey: baseCertBundle.Cert.Subject.CommonName,
+									cmapi.AltNamesAnnotationKey:   strings.Join(baseCertBundle.Cert.DNSNames, ","),
+									cmapi.IPSANAnnotationKey:      strings.Join(utilpki.IPAddressesToString(baseCertBundle.Cert.IPAddresses), ","),
+									cmapi.URISANAnnotationKey:     strings.Join(utilpki.URLsToString(baseCertBundle.Cert.URIs), ","),
+								},
+								Labels: map[string]string{},
+							},
+							Data: map[string][]byte{
+								corev1.TLSCertKey:       baseCertBundle.CertBytes,
+								corev1.TLSPrivateKeyKey: []byte("test-key"),
+								cmmeta.TLSCAKey:         []byte("test-ca"),
+							},
+							Type: corev1.SecretTypeTLS,
+						},
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+
+		"if secret exists, with tls-combined.pem and key.der but only DER Format specified": {
+			certificate: baseCertWithAdditionalOutputFormatDER,
+			certificateOptions: controllerpkg.CertificateOptions{
+				EnableOwnerRef: false,
+			},
+			SecretData: SecretData{Certificate: baseCertBundle.CertBytes, CA: []byte("test-ca"), PrivateKey: baseCertBundle.PrivateKeyBytes},
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{
+					&corev1.Secret{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: gen.DefaultTestNamespace,
+							Name:      "output",
+							Annotations: map[string]string{
+								"my-custom": "annotation",
+							},
+						},
+						Data: map[string][]byte{
+							corev1.TLSCertKey:                  []byte("foo"),
+							corev1.TLSPrivateKeyKey:            []byte("foo"),
+							cmmeta.TLSCAKey:                    []byte("foo"),
+							cmapi.AdditionalOutputFormatDERKey: []byte("foo"),
+							cmapi.AdditionalOutputFormatPEMKey: []byte("foo"),
+						},
+						Type: corev1.SecretTypeTLS,
+					},
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewUpdateAction(
+						corev1.SchemeGroupVersion.WithResource("secrets"),
+						gen.DefaultTestNamespace,
+						&corev1.Secret{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: gen.DefaultTestNamespace,
+								Name:      "output",
+								Annotations: map[string]string{
+									"my-custom": "annotation",
+
+									cmapi.CertificateNameKey:       "test",
+									cmapi.IssuerGroupAnnotationKey: "foo.io",
+									cmapi.IssuerKindAnnotationKey:  "Issuer",
+									cmapi.IssuerNameAnnotationKey:  "ca-issuer",
+
+									cmapi.CommonNameAnnotationKey: baseCertBundle.Cert.Subject.CommonName,
+									cmapi.AltNamesAnnotationKey:   strings.Join(baseCertBundle.Cert.DNSNames, ","),
+									cmapi.IPSANAnnotationKey:      strings.Join(utilpki.IPAddressesToString(baseCertBundle.Cert.IPAddresses), ","),
+									cmapi.URISANAnnotationKey:     strings.Join(utilpki.URLsToString(baseCertBundle.Cert.URIs), ","),
+								},
+								Labels: map[string]string{},
+							},
+							Data: map[string][]byte{
+								corev1.TLSCertKey:                  baseCertBundle.CertBytes,
+								corev1.TLSPrivateKeyKey:            baseCertBundle.PrivateKeyBytes,
+								cmmeta.TLSCAKey:                    []byte("test-ca"),
+								cmapi.AdditionalOutputFormatDERKey: tlsDerContent,
+							},
+							Type: corev1.SecretTypeTLS,
+						},
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+
+		"if secret exists, with tls-combined.pem and key.der but only Combined PEM Format specified": {
+			certificate: baseCertWithAdditionalOutputFormatCombinedPEM,
+			certificateOptions: controllerpkg.CertificateOptions{
+				EnableOwnerRef: false,
+			},
+			SecretData: SecretData{Certificate: baseCertBundle.CertBytes, CA: []byte("test-ca"), PrivateKey: baseCertBundle.PrivateKeyBytes},
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{
+					&corev1.Secret{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: gen.DefaultTestNamespace,
+							Name:      "output",
+							Annotations: map[string]string{
+								"my-custom": "annotation",
+							},
+						},
+						Data: map[string][]byte{
+							corev1.TLSCertKey:                  []byte("foo"),
+							corev1.TLSPrivateKeyKey:            []byte("foo"),
+							cmmeta.TLSCAKey:                    []byte("foo"),
+							cmapi.AdditionalOutputFormatDERKey: []byte("foo"),
+							cmapi.AdditionalOutputFormatPEMKey: []byte("foo"),
+						},
+						Type: corev1.SecretTypeTLS,
+					},
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewUpdateAction(
+						corev1.SchemeGroupVersion.WithResource("secrets"),
+						gen.DefaultTestNamespace,
+						&corev1.Secret{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: gen.DefaultTestNamespace,
+								Name:      "output",
+								Annotations: map[string]string{
+									"my-custom": "annotation",
+
+									cmapi.CertificateNameKey:       "test",
+									cmapi.IssuerGroupAnnotationKey: "foo.io",
+									cmapi.IssuerKindAnnotationKey:  "Issuer",
+									cmapi.IssuerNameAnnotationKey:  "ca-issuer",
+
+									cmapi.CommonNameAnnotationKey: baseCertBundle.Cert.Subject.CommonName,
+									cmapi.AltNamesAnnotationKey:   strings.Join(baseCertBundle.Cert.DNSNames, ","),
+									cmapi.IPSANAnnotationKey:      strings.Join(utilpki.IPAddressesToString(baseCertBundle.Cert.IPAddresses), ","),
+									cmapi.URISANAnnotationKey:     strings.Join(utilpki.URLsToString(baseCertBundle.Cert.URIs), ","),
+								},
+								Labels: map[string]string{},
+							},
+							Data: map[string][]byte{
+								corev1.TLSCertKey:                  baseCertBundle.CertBytes,
+								corev1.TLSPrivateKeyKey:            baseCertBundle.PrivateKeyBytes,
+								cmmeta.TLSCAKey:                    []byte("test-ca"),
+								cmapi.AdditionalOutputFormatPEMKey: []byte(strings.Join([]string{string(baseCertBundle.PrivateKeyBytes), string(baseCertBundle.CertBytes)}, "\n")),
+							},
+							Type: corev1.SecretTypeTLS,
+						},
+					)),
+				},
+			},
+			expectedErr: false,
+		},
 	}
 
 	// TODO: add to these tests once the JKS/PKCS12 support is updated
diff --git a/test/e2e/framework/helper/validation/certificates/certificates.go b/test/e2e/framework/helper/validation/certificates/certificates.go
index 13d8c7778..3d4bae056 100644
--- a/test/e2e/framework/helper/validation/certificates/certificates.go
+++ b/test/e2e/framework/helper/validation/certificates/certificates.go
@@ -22,7 +22,9 @@ import (
 	"crypto/ed25519"
 	"crypto/rsa"
 	"crypto/x509"
+	"encoding/pem"
 	"fmt"
+	"strings"
 
 	"github.com/kr/pretty"
 	corev1 "k8s.io/api/core/v1"
@@ -37,12 +39,21 @@ import (
 // ValidationFunc describes a Certificate validation helper function
 type ValidationFunc func(certificate *cmapi.Certificate, secret *corev1.Secret) error
 
-// Expect2Or3KeysInSecret checks if the secret resource has the correct amount of fields in the secret data
-func Expect2Or3KeysInSecret(_ *cmapi.Certificate, secret *corev1.Secret) error {
-	if !(len(secret.Data) == 2 || len(secret.Data) == 3) {
-		return fmt.Errorf("Expected 2 or 3 keys in certificate secret, but there was %d", len(secret.Data))
+// ExpectValidKeysInSecret checks that the secret contains valid keys
+func ExpectValidKeysInSecret(_ *cmapi.Certificate, secret *corev1.Secret) error {
+	validKeys := [5]string{corev1.TLSPrivateKeyKey, corev1.TLSCertKey, cmmeta.TLSCAKey, cmapi.AdditionalOutputFormatDERKey, cmapi.AdditionalOutputFormatPEMKey}
+	nbValidKeys := 0
+	for k := range secret.Data {
+		for _, k2 := range validKeys {
+			if k == k2 {
+				nbValidKeys++
+				break
+			}
+		}
+	}
+	if nbValidKeys < 2 {
+		return fmt.Errorf("Expected at least 2 valid keys in certificate secret, but there was %d", nbValidKeys)
 	}
-
 	return nil
 }
 
@@ -364,3 +375,40 @@ func ExpectValidBasicConstraints(certificate *cmapi.Certificate, secret *corev1.
 
 	return nil
 }
+
+// ExpectValidAdditionalOutputFormats assert that if additional output formats are requested
+// It contains the additional output format keys in the secret and the content are valid.
+func ExpectValidAdditionalOutputFormats(certificate *cmapi.Certificate, secret *corev1.Secret) error {
+	if len(certificate.Spec.AdditionalOutputFormats) > 0 {
+		for _, f := range certificate.Spec.AdditionalOutputFormats {
+			switch f.Type {
+			case cmapi.AdditionalOutputFormatDER:
+				if derKey, ok := secret.Data[cmapi.AdditionalOutputFormatDERKey]; ok {
+					privateKey := secret.Data[corev1.TLSPrivateKeyKey]
+					block, _ := pem.Decode(privateKey)
+					if !bytes.Equal(derKey, block.Bytes) {
+						return fmt.Errorf("expected additional output Format DER %s to contain the binary formated private Key", cmapi.AdditionalOutputFormatDERKey)
+					}
+				} else {
+					return fmt.Errorf("expected additional output format DER key %s to be present in secret", cmapi.AdditionalOutputFormatDERKey)
+				}
+			case cmapi.AdditionalOutputFormatCombinedPEM:
+				if combinedPem, ok := secret.Data[cmapi.AdditionalOutputFormatPEMKey]; ok {
+					privateKey := secret.Data[corev1.TLSPrivateKeyKey]
+					certificate := secret.Data[corev1.TLSCertKey]
+					expectedCombinedPem := []byte(strings.Join([]string{string(privateKey), string(certificate)}, "\n"))
+					if !bytes.Equal(combinedPem, expectedCombinedPem) {
+						return fmt.Errorf("expected additional output format CombinedPEM %s to contain the combination of privateKey and certificate", cmapi.AdditionalOutputFormatPEMKey)
+					}
+				} else {
+					return fmt.Errorf("expected additional output format CombinedPEM key %s to be present in secret", cmapi.AdditionalOutputFormatPEMKey)
+				}
+
+			default:
+				return fmt.Errorf("unknown additional output format %s", f.Type)
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/test/e2e/framework/helper/validation/validation.go b/test/e2e/framework/helper/validation/validation.go
index 0463927b9..6e20e7be6 100644
--- a/test/e2e/framework/helper/validation/validation.go
+++ b/test/e2e/framework/helper/validation/validation.go
@@ -24,7 +24,7 @@ import (
 
 func DefaultCertificateSet() []certificates.ValidationFunc {
 	return []certificates.ValidationFunc{
-		certificates.Expect2Or3KeysInSecret,
+		certificates.ExpectValidKeysInSecret,
 		certificates.ExpectCertificateDNSNamesToMatch,
 		certificates.ExpectCertificateOrganizationToMatch,
 		certificates.ExpectCertificateURIsToMatch,
@@ -38,6 +38,7 @@ func DefaultCertificateSet() []certificates.ValidationFunc {
 		certificates.ExpectValidPrivateKeyData,
 		certificates.ExpectConditionReadyObservedGeneration,
 		certificates.ExpectValidBasicConstraints,
+		certificates.ExpectValidAdditionalOutputFormats,
 	}
 }
 
@@ -62,7 +63,7 @@ func DefaultCertificateSigningRequestSet() []certificatesigningrequests.Validati
 func CertificateSetForUnsupportedFeatureSet(fs featureset.FeatureSet) []certificates.ValidationFunc {
 	// basics
 	out := []certificates.ValidationFunc{
-		certificates.Expect2Or3KeysInSecret,
+		certificates.ExpectValidKeysInSecret,
 		certificates.ExpectCertificateDNSNamesToMatch,
 		certificates.ExpectCertificateOrganizationToMatch,
 		certificates.ExpectValidAnnotations,
diff --git a/test/e2e/suite/issuers/ca/certificate.go b/test/e2e/suite/issuers/ca/certificate.go
index 119af3016..c47d3defe 100644
--- a/test/e2e/suite/issuers/ca/certificate.go
+++ b/test/e2e/suite/issuers/ca/certificate.go
@@ -25,6 +25,7 @@ import (
 
 	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
+
 	"github.com/jetstack/cert-manager/test/e2e/framework"
 	"github.com/jetstack/cert-manager/test/e2e/util"
 	"github.com/jetstack/cert-manager/test/unit/gen"
@@ -124,6 +125,28 @@ var _ = framework.CertManagerDescribe("CA Certificate", func() {
 			Expect(err).NotTo(HaveOccurred())
 		})
 
+		It("should be able to create a certificate with additional output formats", func() {
+			certClient := f.CertManagerClientSet.CertmanagerV1().Certificates(f.Namespace.Name)
+
+			cert := util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1.IssuerKind, nil, nil)
+			cert.Spec.AdditionalOutputFormats = []v1.AdditionalOutputFormat{
+				{Type: "DER"},
+				{Type: "CombinedPEM"},
+			}
+
+			By("Creating a Certificate")
+			cert, err := certClient.Create(context.TODO(), cert, metav1.CreateOptions{})
+			Expect(err).NotTo(HaveOccurred())
+
+			By("Waiting for the Certificate to be issued...")
+			cert, err = f.Helper().WaitForCertificateReadyAndDoneIssuing(cert, time.Minute*5)
+			Expect(err).NotTo(HaveOccurred())
+
+			By("Validating the issued Certificate...")
+			err = f.Helper().ValidateCertificate(cert)
+			Expect(err).NotTo(HaveOccurred())
+		})
+
 		cases := []struct {
 			inputDuration    *metav1.Duration
 			inputRenewBefore *metav1.Duration
diff --git a/test/unit/gen/certificate.go b/test/unit/gen/certificate.go
index ac172b639..d42a8822f 100644
--- a/test/unit/gen/certificate.go
+++ b/test/unit/gen/certificate.go
@@ -261,3 +261,9 @@ func SetCertificateRevisionHistoryLimit(limit int32) CertificateModifier {
 		crt.Spec.RevisionHistoryLimit = &limit
 	}
 }
+
+func SetCertificateAdditionalOutputFormats(additionalOutputFormats ...v1.AdditionalOutputFormat) CertificateModifier {
+	return func(crt *v1.Certificate) {
+		crt.Spec.AdditionalOutputFormats = additionalOutputFormats
+	}
+}