Key generation does not need a temporary file, and skipping one fixes less than most guides imply. What it removes is a file lifecycle and the cleanup race around it. What it leaves untouched is every opportunity to leak the secret afterwards. The correctness problem in these scripts usually sits elsewhere, because diagnostics and key material travel on different streams.

The file was never the exposure you thought

A temporary file is a concrete risk when it is world-readable, when it survives a crash, or when a cleanup step is skipped after an exception. Those are real, and they are worth fixing. They are also local, visible and easy to test. Name each one in your own script before concluding that the change was cosmetic.

The quieter risks sit downstream: the key printed during debugging, the exception that serialises the whole result object, the crash handler that writes a core dump, the log aggregator that receives the file path and then the contents. Avoiding a temporary file does nothing for any of those, and believing otherwise is how a well-intentioned rewrite leaves the secret more exposed than before. Treat the change as one improvement in a sequence, not as the fix.

What the client writes when no filename is given

Current versions of the client send the generated key to standard output when the key command is given without an output path. That documented behaviour is what makes the in-memory approach viable, and it removes the need for the older trick of pointing the tool at a device path.

import subprocess
proc = subprocess.run(
    ["openvpn", "--genkey", "tls-crypt"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    check=True,
)
key_material = proc.stdout

Pass the command as a list. There is no shell expansion needed here, and enabling a shell would make the argument boundary depend on quoting you do not control. Use the intended executable by absolute path if the script may run with a different search path than you expect.

The return-code check keeps the success path from continuing after a failed invocation. Without it, an empty byte string can flow onward and only fail much later, at the point where the key is used and the error message no longer mentions generation at all.

Two streams, two meanings

Standard output carries the key. Standard error carries everything the tool wants to tell you, including warnings about the configuration, the version and the key type. Redirecting both into one buffer is the mistake that produces a plausible-looking key with a sentence stitched into the middle of it.

Keep the failure path separate as well. When the call fails, report the command that failed and the exit status, and then decide deliberately whether any of standard error is safe to show. Dumping the full result object to a log is the usual shortcut, and it can carry the key with it.

A binary-safe read matters if you later hash the material or compare it. Text mode with an unexpected encoding can alter bytes that were never meant to be text, and a changed key is indistinguishable from a wrong one until a handshake fails.

If you do persist it, do it deliberately

Most deployments eventually need the key on disk, on the gateway or in a secret store. Write it once, with restrictive permissions from creation rather than a later chmod, and make the write atomic so a partial file never becomes the one a service reads at start-up. A rename from a path in the same directory is usually enough for that. Remove the staging path on the failure branch, since the failed write is exactly the copy people forget.

Set the file mode when the file is created, and keep the directory mode restrictive as well. Avoid any path under a location that a backup job sweeps up without thinking, because a secret in a nightly backup is a secret in every copy of that backup. The same reasoning applies to a shared volume that other services can read.

A shared key is not an identity

Generating this material does not issue a certificate, does not enrol a client and does not establish who is connecting. The shared key protects a channel between parties that already trust each other; identity still comes from the certificate and whatever authentication sits in front of it.

The key types are also not interchangeable. A symmetric key for one feature is not a drop-in for another, and newer releases add their own variants with different files at each end. Generate the type your configuration actually names, then use it in the role that type defines.

Test the result with a real connection rather than by inspecting the file size. If the handshake completes and the logs show no key warnings, the generation path is right; if the key were truncated by a merged stream, the connection would fail in a way that points at the peer instead of the script.

Amanqephe ahlobene