create_self_signed_cert

This script will create a private key key.pem and a self-signed certificate cert.pem in the given directory ($PWD if not given).

The given directory will be created if it does not exit yet.

The optional second positive integer parameter (range: [1, 24855]) specifies the number of days the generated certificate is valid for; the default is 30 days.

The optional third parameter is the common name (localhost if not given) of the certificate to be added.

On macOS, the certificate will be added to the "login" keychain also.

The certificate created by this script is useful if you do not use mutual TLS, the HTTP-client can be configured to ignore self-signed certificates, the server’s certificate verifier supports using a trust anchor as both a CA certificate and an end-entity certificate, or if you can add the certificate to your trust store.

$ curl --insecure ...
$ wget --no-check-certificate ...
$ http --verify=no ...

Chrome and Safari need no further configuration—​you should restart your browser though.

For Firefox the created certificate has to be accepted manually.

Docker needs to be restarted.

Ensure that the common name (set via the third parameter of this script) of the generated certificate has an entry in /etc/hosts.

WARNING: /etc/hosts does not have an entry for '127.0.0.1 localhost https.internal'
/etc/hosts
127.0.0.1 localhost

/etc/hosts
127.0.0.1 localhost https.internal

Both key.pem and cert.pem should not be checked into version control!

If the given directory is inside a Git working tree the script will offer to modify the .gitignore file:

WARNING: key.pem and/or cert.pem is not ignored in '/Users/example/tmp/.gitignore'

Do you want me to modify your .gitignore file (Y/N)?

Related Script: git-cleanup

Certificates with more than 180 days validity will not be accepted by the Apple platform or Safari.

Copy this script (and its related delete, renew, and verify scripts) into your Node.js project and add it as a custom script to your package.json file:

package.json
{
...
  "scripts": {
    "cert:create": "scripts/create_self_signed_cert.sh certs",
    "cert:delete": "scripts/delete_self_signed_cert.sh certs",
    "cert:renew": "scripts/renew_self_signed_cert.sh certs",
    "cert:verify": "scripts/verify_self_signed_cert.sh certs",
...
  }
}
$ npm run cert:create
$ npm run cert:delete
$ npm run cert:renew
$ npm run cert:verify

Usage

$ scripts/cert/create_self_signed_cert.sh
Adding 'localhost' certificate (expires on: 2024-02-29) to keychain /Users/example/Library/Keychains/login.keychain-db ...
$ date -Idate
2024-01-30
$ stat -f '%A %N' *.pem
600 cert.pem
600 key.pem
$ openssl x509 -ext subjectAltName -noout -in cert.pem
X509v3 Subject Alternative Name:
    DNS:localhost
$ openssl x509 -startdate -noout -in cert.pem
notBefore=Jan 30 16:25:43 2024 GMT
$ openssl x509 -enddate -noout -in cert.pem
notAfter=Feb 29 16:25:43 2024 GMT

$ scripts/cert/create_self_signed_cert.sh dist/etc/nginx
Adding 'localhost' certificate (expires on: 2024-02-29) to keychain /Users/example/Library/Keychains/login.keychain-db ...

$ scripts/cert/create_self_signed_cert.sh . 10
Adding 'localhost' certificate (expires on: 2024-02-09) to keychain /Users/example/Library/Keychains/login.keychain-db ...

$ scripts/cert/create_self_signed_cert.sh ~/.local/secrets/certs/https.internal 20 https.internal
Adding 'https.internal' certificate (expires on: 2024-02-19) to keychain /Users/example/Library/Keychains/login.keychain-db ...

macOS

Check your "login" keychain in Keychain Access; Secure Sockets Layer (SSL) should be set to "Always Trust":

self signed macos

Firefox (MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT)

You need to bypass the self-signed certificate warning by clicking on "Advanced" and then "Accept the Risk and Continue":

self signed firefox

Examples

Apache HTTP Server

$ scripts/cert/create_self_signed_cert.sh ~/.local/secrets/certs/localhost
$ docker run --rm httpd:2.4.62-alpine3.20 cat /usr/local/apache2/conf/httpd.conf > httpd.conf.orig
$ sed -e 's/^#\(Include .*httpd-ssl.conf\)/\1/' \
      -e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' \
      -e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' \
      httpd.conf.orig > httpd.conf
$ mkdir -p htdocs
$ printf '<!doctype html><title>Test</title><h1>Test</h1>' > htdocs/index.html
$ docker run -i -t --rm -p 3000:443 \
  -v "$PWD/htdocs:/usr/local/apache2/htdocs:ro" \
  -v "$PWD/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro" \
  -v "$HOME/.local/secrets/certs/localhost/cert.pem:/usr/local/apache2/conf/server.crt:ro" \
  -v "$HOME/.local/secrets/certs/localhost/key.pem:/usr/local/apache2/conf/server.key:ro" \
  httpd:2.4.62-alpine3.20

nginx

$ scripts/cert/create_self_signed_cert.sh ~/.local/secrets/certs/localhost
$ printf 'server {
  listen 443 ssl;
  listen [::]:443 ssl;
  ssl_certificate /etc/ssl/certs/server.crt;
  ssl_certificate_key /etc/ssl/private/server.key;
  location / {
    root   /usr/share/nginx/html;
    index  index.html;
  }
}' > nginx.conf
$ mkdir -p html
$ printf '<!doctype html><title>Test</title><h1>Test</h1>' > html/index.html
$ docker run -i -t --rm -p 3000:443 \
  -v "$PWD/html:/usr/share/nginx/html:ro" \
  -v "$PWD/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
  -v "$HOME/.local/secrets/certs/localhost/cert.pem:/etc/ssl/certs/server.crt:ro" \
  -v "$HOME/.local/secrets/certs/localhost/key.pem:/etc/ssl/private/server.key:ro" \
  nginx:1.27.1-alpine3.20-slim

Go

func main() {
  const port = 3000

  server := http.Server{
    Addr:         fmt.Sprintf(":%d", port),
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 5 * time.Second,
    IdleTimeout:  5 * time.Second,
    Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
      _, err := w.Write([]byte("<!doctype html><title>Test</title><h1>Test</h1>"))
      if err != nil {
        slog.Error("handle response", slog.Any("error", err))
      }
    }),
  }
  defer func(server *http.Server) {
    if err := server.Close(); err != nil {
      slog.Error("server close", slog.Any("error", err))
      os.Exit(70)
    }
  }(&server)

  slog.Info(fmt.Sprintf("Listen local: https://localhost:%d", port))

  if err := server.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
    slog.Error("listen", slog.Any("error", err))
    os.Exit(70)
  }
}
$ cd scripts/cert/go/stdlib
$ ../create_self_signed_cert.sh
$ go run server.go

NodeJS

['uncaughtException', 'unhandledRejection'].forEach((s) =>
  process.once(s, (e) => {
    console.error(e);
    process.exit(70);
  }),
);
['SIGINT', 'SIGTERM'].forEach((s) => process.once(s, () => process.exit(0)));

let https;
try {
  https = await import('node:https');
} catch {
  console.error('https support is disabled');
  process.exit(78);
}

const port = 3000;

const server = https.createServer(
  {
    key: readFileSync('key.pem'),
    cert: readFileSync('cert.pem'),
  },
  (_, w) => {
    w.writeHead(200).end('<!doctype html><title>Test</title><h1>Test</h1>');
  },
);
server.keepAliveTimeout = 5000;
server.requestTimeout = 5000;
server.timeout = 5000;
server.listen(port);

console.log(`Listen local: https://localhost:${port}`);
$ cd scripts/cert/js/nodejs
$ ../create_self_signed_cert.sh
$ node server.mjs

Java

public final class Server {

  public static void main(String[] args) throws Exception {
    var port = 3000;

    var server =
        HttpsServer.create(
            new InetSocketAddress(port),
            0,
            "/",
            exchange -> {
              var response = "<!doctype html><title>Test</title><h1>Test</h1>";
              exchange.sendResponseHeaders(HTTP_OK, response.length());
              try (var body = exchange.getResponseBody()) {
                body.write(response.getBytes());
              } catch (IOException e) {
                LOGGER.log(SEVERE, "handle response", e);
              }
            });
    server.setHttpsConfigurator(new HttpsConfigurator(newSSLContext()));
    server.setExecutor(newVirtualThreadPerTaskExecutor());
    server.start();

    LOGGER.info(format("Listen local: https://localhost:%d", port));
  }

  static {
    System.setProperty("sun.net.httpserver.maxReqTime", "5");
    System.setProperty("sun.net.httpserver.maxRspTime", "5");
    System.setProperty("sun.net.httpserver.idleInterval", "5000");
  }

  private static final Logger LOGGER = getLogger(MethodHandles.lookup().lookupClass().getName());

  private static SSLContext newSSLContext() throws Exception {
    var keyStorePath = requireNonNull(getenv("KEYSTORE_PATH"), "keystore path");
    var keyStorePassword =
        requireNonNull(getenv("KEYSTORE_PASS"), "keystore password").toCharArray();

    var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    keyStore.load(newInputStream(Path.of(keyStorePath)), keyStorePassword);

    var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, keyStorePassword);

    var trustManagerFactory =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(keyStore);

    var sslContext = SSLContext.getInstance("TLS");
    sslContext.init(
        keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

    return sslContext;
  }
}
$ cd scripts/cert/java/stdlib
$ ../create_self_signed_cert.sh
$ openssl pkcs12 -export -in cert.pem -inkey key.pem -out certificate.p12 -name localhost -password pass:changeit
$ keytool -importkeystore -srckeystore certificate.p12 -srcstoretype pkcs12 -srcstorepass changeit -destkeystore keystore.jks -deststorepass changeit
$ KEYSTORE_PATH=keystore.jks KEYSTORE_PASS=changeit java Server.java

Spring Boot

@SpringBootApplication
public class Server {

  @RestController
  static class Controller {

    @GetMapping("/")
    public String index() {
      return "<!doctype html><title>Test</title><h1>Test</h1>";
    }
  }

  public static void main(String[] args) {
    SpringApplication.run(Server.class, args);
  }
}
server.port=3000
server.tomcat.connection-timeout=5s
server.ssl.bundle=https
spring.ssl.bundle.pem.https.reload-on-update=true
spring.ssl.bundle.pem.https.keystore.certificate=cert.pem
spring.ssl.bundle.pem.https.keystore.private-key=key.pem
$ cd scripts/cert/java/spring-boot
$ ../create_self_signed_cert.sh
$ ./gradlew bootRun

Quarkus

Instead of using this script, you might want to use Quarkus' own certificate tooling.

@Path("/")
public class Server {

  @GET
  @Produces(TEXT_HTML)
  @RunOnVirtualThread
  public String index() {
    return "<!doctype html><title>Test</title><h1>Test</h1>";
  }
}
quarkus.http.ssl-port=3000
quarkus.http.idle-timeout=5s
quarkus.http.read-timeout=5s
quarkus.http.ssl.certificate.reload-period=30s
quarkus.http.ssl.certificate.files=cert.pem
quarkus.http.ssl.certificate.key-files=key.pem
$ cd scripts/cert/java/quarkus
$ ../create_self_signed_cert.sh
$ ./gradlew quarkusDev

Prerequisites