# Sign a CSR with your own CA

To sign a CSR with your own CA, run `openssl x509 -req -in request.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out certificate.crt -days 397 -sha256 -copy_extensions copyall`. The `-copy_extensions` flag is essential: without it OpenSSL silently discards every extension in the CSR, including the subject alternative names, and the resulting certificate is rejected by every browser. Safer still is to ignore the CSR's extensions entirely and supply your own with `-extfile`, because `copyall` lets the requester dictate what the certificate claims.

Source: https://certguru.io/openssl/sign-csr-with-ca/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

## Look at the request first

You are about to vouch for whatever this says. Read it:

```bash
openssl req -in request.csr -noout -text -verify
```

Confirm the subject is who you think it is and the requested SANs are names this requester should be
allowed to have.

## The quick way

*Sign, copying the CSR's extensions*

```bash
openssl x509 -req -in request.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out certificate.crt -days 397 -sha256 \
  -copy_extensions copyall
```

| Placeholder | What to put there |
| --- | --- |
| `-req` | The input is a CSR rather than a certificate. |
| `-CA / -CAkey` | The issuing CA certificate and its private key. |
| `-CAcreateserial` | Create or increment the serial number file so serials stay unique. |
| `-copy_extensions copyall` | Carry the CSR extensions across. Without this the SANs are lost. |
| `-days 397` | Validity. 397 days is the public-CA maximum and a reasonable internal ceiling. |

> **copyall trusts the requester**
>
> It copies every extension from the CSR verbatim — including `basicConstraints=CA:TRUE` if someone
>   put it there. That would issue them a certificate capable of signing others, which every machine
>   trusting your root would honour. Use `copyall` only for CSRs you generated yourself.

## The safe way — define the extensions yourself

*ext.cnf*

```ini
basicConstraints       = critical,CA:FALSE
keyUsage               = critical,digitalSignature,keyEncipherment
extendedKeyUsage       = serverAuth,clientAuth
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName         = DNS:service.internal, DNS:*.service.internal
```

*Sign using your extensions, ignoring the CSR's*

```bash
openssl x509 -req -in request.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out certificate.crt -days 397 -sha256 \
  -extfile ext.cnf
```

This is how a real CA behaves: the request is an application, and the issuer decides what the
certificate actually says.

## What each extension does

| Extension | Why it is there |
| --- | --- |
| `basicConstraints=CA:FALSE` | This certificate may not sign other certificates |
| `keyUsage` | Restricts the key to signatures and key exchange |
| `extendedKeyUsage=serverAuth` | Valid for TLS servers. Add `clientAuth` for mTLS |
| `subjectKeyIdentifier` | Identifies this certificate's key |
| `authorityKeyIdentifier` | Links back to the issuing CA, so clients can build the chain |
| `subjectAltName` | The hostnames. The only thing browsers match against |

## Verify what you issued

```bash
openssl x509 -in certificate.crt -noout -subject -issuer -dates -ext subjectAltName -ext basicConstraints
```

Then confirm it actually chains to your CA:

```bash
openssl verify -CAfile ca.crt certificate.crt
```

Expected output:

```
certificate.crt: OK
```

> **Check the SANs every single time**
>
> The extension-dropping failure produces a certificate that looks completely normal in casual
>   inspection and fails in every browser. One `-ext subjectAltName` check catches it before it
>   reaches a server.

## Building the chain file for the server

The server needs its own certificate plus every certificate between it and the root:

```bash
cat certificate.crt ca.crt > fullchain.crt
```

If you signed with an [intermediate](/openssl/create-intermediate-ca/), the intermediate goes in the
chain and the root does not — clients that trust the root do not need to be sent it.


## Frequently asked questions

### Why does my signed certificate have no SANs?

Because `openssl x509 -req` drops all CSR extensions unless told otherwise. Add `-copy_extensions copyall`, or better, supply the extensions yourself with `-extfile`. This is the single most common mistake when running a private CA.

### What does -CAcreateserial do?

It creates a serial number file next to the CA certificate, starting at a random value, so each issued certificate gets a unique serial. After the first issuance the file exists and OpenSSL increments it. Duplicate serials from one CA break revocation and confuse clients.

### Is -copy_extensions copyall safe?

Not entirely. It copies whatever the requester put in the CSR, including `basicConstraints CA:TRUE` if they were malicious or careless — which would let their certificate sign others. For a CA that accepts requests from anyone, define the extensions yourself with `-extfile`.

### How long should the certificate be valid?

397 days is the maximum public CAs may issue and a sensible internal ceiling too. Shorter validity limits the damage from a key exposure you have not noticed, and forces you to keep the renewal process working.


## References

- [openssl-x509(1)](https://docs.openssl.org/3.5/man1/openssl-x509/) — -req signs a CSR with no CA database
- [openssl-ca(1)](https://docs.openssl.org/3.5/man1/openssl-ca/) — the alternative that maintains an index and serial file

## Related pages

- https://certguru.io/openssl/create-root-ca/
- https://certguru.io/openssl/generate-csr-with-san/
- https://certguru.io/openssl/create-intermediate-ca/
- https://certguru.io/concepts/certificate-chain/
- https://certguru.io/concepts/san/

--
CertGuru — https://certguru.io
