It turns out that (with reasonably new versions of OpenSSL) generating a private key and PKCS#10 Certificate Signing Request is not as difficult as it first appears.
Private Key
To generate an RSA private key:
$ (umask 077 && openssl genpkey -out foo.key -algorithm RSA)
To specify the key size, add -pkeyopt rsa_keygen_bits:4096.
or for an ED25519 private key:
$ (umask 077 && openssl genpkey -out foo.key -algorithm ED25519)
Certificate Signing Request
To generate a CSR:
$ openssl req -new -key foo.key -subj /CN=foo.example.com
There are no prompts, and you don't need to provide input via a config file.
To add a DNS-ID to the certificate, add -addext 'subjectAltName = DNS:foo.example.com'.
Add -text if you want a human-readable form of the CSR to be printed to stdout. Use this while experimenting with other options to check that they are adding the correct bits to the CSR.
All in one
Both key and CSR generation can be done in one command:
$ openssl req -newkey rsa:3072 -keyout foo.rsa -nodes -subj /CN=foo.example.com
or
$ openssl req -newkey ed25519 -keyout foo.rsa -nodes -subj /CN=foo.example.com
req is even smart enough to set the correct (0600) permissions on the private key file, so you don't need to bother with the umask dance.