Self-signed certificates: three cases where they are right
A self-signed certificate is one where the subject and the issuer are the same entity. Nobody vouched for it except itself, which is why every browser refuses it and every article says do not use one.
The advice is correct for public websites and it is repeated well beyond that boundary, usually with a justification that is not quite true. Getting the justification right is what tells you which of the three exceptions apply.
What it does and does not give you
The common defence is that a self-signed certificate still encrypts the traffic, so it is better than plain HTTP. That is half right and the missing half matters.
The encryption is real. What is missing is authentication, and without authentication the encryption is defeatable by anyone able to sit in the path, because they simply present their own self-signed certificate and you have no way to tell the difference. Both are untrusted, both produce the same browser warning, and a user who has been trained to click through one warning will click through the other.
So the accurate statement is that a self-signed certificate protects against passive listening and not against an active intermediary. Whether that is acceptable depends entirely on whether an active intermediary is plausible on the path in question, which is exactly what separates the three exceptions from everything else.
The distinction that resolves most of the argument
Self-signed and private certificate authority are used interchangeably and they are different things.
A self-signed certificate is trusted by installing that one certificate on every machine that needs to talk to that one service. Ten services and forty machines is four hundred installations, and each certificate renewal repeats them.
A private CA is a root you generate once and install once per machine, which then signs a certificate per service. New service, no client-side change. Renewal, no client-side change. The same operational shape as the public web, with you as the authority.
Almost every situation where somebody reaches for a self-signed certificate at scale is one where a private CA is the correct answer instead, and the cost difference is one extra step at the beginning.
Three cases where it is right
A device with no public name. A router's management page, a NAS, an out-of-band management controller, a printer. A public authority cannot issue for these because there is no public hostname to validate and no way to prove control of anything. The device ships with a self-signed certificate, and the correct handling is to record its fingerprint once and verify that the fingerprint has not changed, rather than clicking through the warning every time.
echo | openssl s_client -connect 192.168.1.1:443 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256
That is the whole security model for this case: trust on first use, then notice if it changes. It is weak, and it is stronger than typing a password over plain HTTP because someone told you self-signed is bad.
Machine-to-machine traffic where both ends are yours. A backend calling another backend, a database connection, a message broker. Here you control the client, which means you can pin exactly which certificate is acceptable rather than accepting anything a public authority signed. That is a stronger guarantee than the public web has, because a public trust store contains hundreds of authorities and any of them can issue for your name.
This is also where mutual authentication belongs, and where a private CA earns its place immediately once there is more than one service.
Local development and automated tests. A certificate for a hostname that exists only inside a container network cannot be publicly issued and does not need to be. What matters is that the development setup exercises TLS at all, because a stack that runs plaintext locally and TLS in production is a stack where the first TLS problem is discovered by users.
Where it is wrong, and one case that looks like an exception
Anything a browser reaches. A staging site, an admin panel, an internal tool on a public hostname, a client's intranet reachable over the internet. In every one of these a human is being asked to evaluate a certificate warning, and humans are not able to do that reliably, which is the actual reason browsers make the warning hostile.
The case that gets argued is the internal tool, on the grounds that only staff use it. It is the weakest place to accept a warning, because staff use it daily, and daily exposure to a warning is training. After a fortnight they click through without reading, and they will click through on a different site too.
The practical response is that this exception has largely stopped existing. A publicly resolvable hostname can be validated through DNS without the service being reachable from the internet, which means an internal-only tool can hold a publicly trusted certificate. The mechanics of getting one that way, and the specific failure modes of the automation, are in renewal failures in production.
What a self-signed certificate looks like when you are diagnosing one
Two signatures, and knowing them saves the ten minutes people spend deciding whether a certificate is broken or merely untrusted.
In the browser, the error names the authority rather than the certificate: a message about the issuer being unknown, rather than about a date or a name mismatch. Which of the eight possible faults produced the same red page is decided by that code and nothing else: read the second line, not the headline.
From the command line, the subject and the issuer are identical, and the verification result is non-zero:
echo | openssl s_client -connect example.internal:443 2>/dev/null \
| openssl x509 -noout -subject -issuer
If those two lines match, it signed itself. If they differ but verification still fails, you have a different problem entirely, most often a missing intermediate, which behaves like an untrusted certificate in some clients and works fine in others: the three commands that separate the two.
The part that catches agencies
Self-signed certificates on internal services expire like every other certificate, and they expire with nobody watching, because the whole point of the service is that no customer sees it.
A backup target, a monitoring endpoint, an internal API. The certificate lapses, the client that was pinning it starts refusing connections, and the failure surfaces as something entirely unrelated: backups stop, or a synchronisation silently returns nothing. Nothing about the symptom points at a certificate, and no public monitoring service can see the host to warn you.
Which means the expiry has to be tracked deliberately, on a list that includes the certificates nobody outside the building can reach. That list is usually the shortest and most neglected part of a portfolio inventory, and it is the one where a lapse produces a diagnosis measured in hours: the alert threshold that gives you room to act.