The router has a web interface, an API, maybe a VPN: all of it runs over HTTPS. But with which certificate? Out of the box RouterOS uses a self-signed one, and the browser complains on every visit. In this article we get a real certificate from Let’s Encrypt — trusted by every browser, free and automatic — using the ACME client built into RouterOS 7. We do it for real in the lab: we open the right port, watch packet by packet what happens on the wire, run into two instructive failures, and in the end we dissect the certificate we obtained all the way down to the root of its chain of trust.
Background reference: Certificates, CAs and the chain of trust — what a CA is, what “chain” means, DV vs OV/EV, OCSP/CRL, Certificate Transparency and CAA. Here we take those terms as understood.
Step 1: what we get, and what you really need
ACME (Automatic Certificate Management Environment, RFC 8555) is the protocol a server uses to ask a Certificate Authority for a certificate by proving it controls the domain, with no manual steps. Let’s Encrypt is the most used free CA in the world and speaks ACME. RouterOS 7 has the ACME client built in: no external software.
There is, though, an honest price to state up front, because it is the heart of the whole article. To give you the certificate, Let’s Encrypt must verify that the name is yours. The method we’ll use (the http-01 challenge) works like this: Let’s Encrypt connects to your name on port 80 and expects to find there a file only you could have placed. So two things are needed:
- a public DNS name pointing to the router (your own domain, or the MikroTik IP Cloud name);
- port 80 reachable from the Internet to the router — directly, or via a port forward (DST-NAT) on the edge router.
Opening port 80 to the world is not free in terms of risk: we’ll see it literally in the sniffer at Step 5. That’s why we open it only for the issuance window and close it afterwards (Step 9).
Step 2: the command
On modern RouterOS 7 (here a 7.25 stable) the command is add-acme. A single line:
/certificate/add-acme \
directory-url=https://acme-v02.api.letsencrypt.org/directory \
domain-names=chr01test.mikrotikdojo.com
The directory-url is the entry point of the Let’s Encrypt API (from there the router discovers all the other endpoints). domain-names is the name (or names) we want the certificate for. There is also an older command, enable-ssl-certificate, tied to IP Cloud: it works, but add-acme is the current one and handles automatic renewal on its own.
For it to succeed, the router’s www service must be up on port 80: it’s the one that will answer the challenge.
Step 3: the http-01 challenge, told as a story
When you run the command, a little ceremony unfolds behind the scenes. The router knocks on the Let’s Encrypt API and says: “I’d like a certificate for chr01test.mikrotikdojo.com“. Let’s Encrypt answers with a token — a random string — and a task: “make it available at http://chr01test.mikrotikdojo.com/.well-known/acme-challenge/<token>, then tell me”.
Here the router’s www service enters the scene. It needs to do nothing special: the ACME client hands it the token, and www keeps it ready at that precise address like a concierge holding an envelope with your name on it. When Let’s Encrypt comes knocking on port 80 and asks for that very envelope, the concierge hands it over. The token the router delivers is not the bare token: it is token.key-thumbprint — the answer is mathematically bound to the router’s private key, so nobody else, even knowing the token, could forge the right answer. Let’s Encrypt compares it with what it expects; if it matches, it is convinced the name is really yours, and it signs the certificate.
This whole exchange — the knock, the token, the envelope on port 80 — happens in plain HTTP. And it is precisely because it is in the clear that we can watch it packet by packet.
Step 4: under the microscope — and the first failure (behind NAT)
We put a sniffer on the router’s Internet-facing interface and run the command. The first attempt ends like this:
/certificate print detail
0 Ea acme-status="http-01 challenge validation failed,
urn:ietf:params:acme:error:connection - Fetching
http://chr01test.mikrotikdojo.com/.well-known/acme-challenge/0WwwuU4...PGwU:
Error getting validation data"
Let’s look at the packets, not the words. The outbound direction works: the router reaches the Let’s Encrypt API on 443 (172.65.32.248), opens the dialogue, creates the account and gets the token.
# router → Let's Encrypt API (443): connection open
router.33768 > 172.65.32.248.443: Flags [S] # SYN to the API
172.65.32.248.443 > router.33768: Flags [S.] # SYN-ACK: handshake
The problem is the inbound direction. And in the sniffer you can even see a Let’s Encrypt validator (51.178.174.226) trying — and failing: 320 connection attempts on port 80, not one that goes through, and zero challenge GETs completed.
# a Let's Encrypt validation node knocks on :80... in vain
51.178.174.226.51xxx > router.80: Flags [S] # SYN \
51.178.174.226.51xxx > router.80: Flags [S] # SYN } 320 SYNs, never a completed handshake
51.178.174.226.51xxx > router.80: Flags [S] # SYN /
# GET /.well-known/acme-challenge/... received and served: 0
That explains Error getting validation data: the envelope wasn’t delivered because the port-80 forward from outside wasn’t delivering yet (the SYN arrives, but the router’s reply doesn’t come back → the handshake never closes). Lesson one: the challenge has two directions — outbound to the API (plain NAT is enough) and inbound on 80 (you need the port forward, done right). We fix it and try again.
Step 5: open port 80… and the world notices within seconds
With port 80 forwarded, we relaunch. And here something happens that is worth the whole article on its own. Let’s look at who knocks on port 80 the moment it becomes public:
# SYNs arriving on port 80, counted by source IP (from the sniffer)
1474 40.160.24.53 <-- a single IP: 1474 connection attempts
14 44.252.14.108
7 47.129.199.229
7 13.60.43.228
7 13.51.178.31
The IPs making 6–14 attempts and completing the connection are the Let’s Encrypt validation nodes (we’ll come back in a moment to why there are so many). But that first one — 1474 SYNs from a single address — is not Let’s Encrypt: it’s a scanner. The moment a port 80 appears on the Internet, the bots that sweep the network find it in seconds and start hammering it. This isn’t theory: it happened live, in our lab, the first time we opened up.
And the hammering has a side effect that makes issuance fail. The RouterOS www service has a cap on concurrent sessions (max-sessions, 20 by default). The scanner’s flood saturates it; above the cap the router refuses the excess connections with an RST. And among the refused ones ends up one of the Let’s Encrypt validation perspectives. Result:
acme-status="http-01 challenge validation failed,
... During secondary validation: ...
Timeout during connect (likely firewall problem)"
And you can see it, packet by packet — it’s plain HTTP, headers and body. A real Let’s Encrypt node asks for the challenge, and the router serves it the token:
# the challenge requested by an LE node
23.178.112.104 > router:80
GET /.well-known/acme-challenge/GBjyI0nclef5ifJguzhLO56dde_AMz8PDukkBYRiR3Q HTTP/1.1
Host: chr01test.mikrotikdojo.com
User-Agent: Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)
Accept: */*
# the router replies, and the BODY carries the token (token . key-thumbprint)
router:80 > 23.178.112.104
HTTP/1.1 200 OK
Content-Length: 87
GBjyI0nclef5ifJguzhLO56dde_AMz8PDukkBYRiR3Q.NGo2y4WTCYleqL9xJ957-d1OkuWVBem0EJqYT4SGOqs
\_________ token _________________________/ \__________ key thumbprint ______________/
But the port-80 counts tell the story of the crowd — and why one perspective is left out:
inbound SYN : 1539 (1474 of them from a single scanner, 40.160.24.53)
SYN-ACK : 736
RST from router : 796 <-- connections cut off beyond max-sessions=20
The concierge works (it serves the 200 OK to whoever manages to talk to it); it’s not a misconfiguration, it’s the crowd at the door that stops it serving everyone — and among those not served is a Let’s Encrypt validation perspective.
Why Let’s Encrypt validates from multiple points: MPIC
Here the “secondary validation” needs explaining. Until a few years ago a CA validated from a single point of the network. It was a huge weak spot. The Internet routes packets with BGP, a protocol that runs on trust: a network announces “the block of addresses X is reached through me” and the others believe it. A malicious network can announce a victim’s prefix — this is a BGP hijack — and for a few minutes attract that traffic to itself. If it does so during validation, the CA’s validator ends up on the attacker’s server, which answers the challenge and walks away with a real, trusted certificate for a domain that isn’t its own.
The countermeasure is MPIC (Multi-Perspective Issuance Corroboration), adopted by Let’s Encrypt between 2024 and 2025 and now required by the CA/Browser Forum. Let’s Encrypt doesn’t validate from a single place: it does so from multiple perspectives spread across different networks, clouds and regions (that’s why in the sniffer we see many IPs knocking). A BGP hijack is usually local: it propagates to part of the Internet, not all of it. The “distant” perspectives, which still reach the real server, disagree with the hijacked one → no quorum → the CA refuses. To beat MPIC you’d need a near-global hijack: huge and extremely noisy.
The twist: it is this very shield that made us fail above. One perspective couldn’t connect (our fault, the saturated www) and the anti-hijack logic, lacking full corroboration, said no. The mechanism that stops attackers also stopped our clogged port.
Step 6: the wrong temptation, and two real defenses
The instinctive reaction is: “then I’ll accept only Let’s Encrypt IPs on port 80″. You can’t, and it’s by design. Let’s Encrypt does not publish the list of IPs it validates from, precisely because of MPIC: the perspectives change without notice and are distributed on purpose. An address-list = "Let's Encrypt" would break at the first change — and they explicitly discourage it.
The right defense doesn’t filter by source, but by behavior: whoever opens too many concurrent connections to port 80 is, most likely, a scanner; the Let’s Encrypt nodes open only a few each. A rule puts whoever floods into a dynamic list and drops them, without touching validation:
/ip firewall filter add chain=input protocol=tcp dst-port=80 \
connection-state=new connection-limit=20,32 \
action=add-src-to-address-list address-list=port80-flood \
address-list-timeout=10m
/ip firewall filter add chain=input protocol=tcp dst-port=80 \
src-address-list=port80-flood action=drop
Whoever exceeds 20 concurrent new connections from the same address (the ,32 counts per /32, i.e. per single IP) ends up in the port80-flood list and is dropped for 10 minutes. We put it to the test in the lab with a real flood, and it catches:
/ip firewall address-list print where list=port80-flood
# LIST ADDRESS TIMEOUT
0 D port80-flood 198.51.100.7 8m24s <-- listed by behavior, not by known IP
/ip firewall filter print stats
# CHAIN ACTION PACKETS
0 input add-src-to-address-list 80
1 input drop 475 <-- flood dropped
As an alternative (or in addition), the simplest stopgap is to raise the www concurrent-session cap, so even the residual noise doesn’t smother the LE nodes:
/ip service set www max-sessions=100
We relaunch issuance. This time all the perspectives pass and the log shows the line we were waiting for:
certificate,info acme cert 'acme_cert_chr01test.mikrotikdojo.com' imported,
next update scheduled at 2027-01-10T...Z
Done: certificate issued and imported, with renewal already scheduled on its own. (The way to avoid the open port entirely exists — the DNS-01 challenge, a TXT record instead of port 80 — and we cover it in the KB.)
Step 7: the certificate, dissected
Now the interesting part: what Let’s Encrypt actually gave us. Let’s pull out the certificate’s fields (here read with openssl, but RouterOS shows them too):
subject CN = chr01test.mikrotikdojo.com
issuer C = US, O = Let's Encrypt, CN = YR1
validity 89 days (Not Before ... Not After, ~3 months)
key RSA 2048 bit · signature sha256WithRSAEncryption
X509v3 Subject Alternative Name: DNS:chr01test.mikrotikdojo.com
X509v3 Key Usage (critical): Digital Signature, Key Encipherment
X509v3 Extended Key Usage: TLS Web Server Authentication
X509v3 Basic Constraints (critical): CA:FALSE
X509v3 Certificate Policies: 2.23.140.1.2.1
Authority Information Access: CA Issuers - URI:http://yr1.i.lencr.org/
X509v3 CRL Distribution Points: http://yr1.c.lencr.org/5.crl
CT Precertificate SCTs: (2 Signed Certificate Timestamp)
Five things to read carefully, all very current:
- ~90-day validity. Short on purpose: the less time a certificate lives, the less damage it does if the key is stolen. That’s why renewal must be automatic.
- Identity only via SAN. The valid name is in the Subject Alternative Name, not in the old Common Name: that’s how modern browsers read certificates.
- Policy
2.23.140.1.2.1. It’s the CA/Browser Forum OID for Domain Validated (DV): Let’s Encrypt certifies that you control the domain, not who you are. No organization identity (that’s its limit, the KB covers it). - No OCSP, only CRL. The Authority Information Access has only “CA Issuers”, no OCSP URL: Let’s Encrypt retired OCSP in 2025 and bets on CRLs and short-lived certificates. A very recent detail that you’ll still find explained the other way around online.
- Two Certificate Transparency SCTs. Embedded in the certificate: they prove it was recorded in public logs before issuance. That’s why a mis-issuance can’t stay hidden — anyone can watch the CT logs for their own domain.
Practical note: our chr01test.mikrotikdojo.com is now, from this moment, in the public CT logs forever. It’s no secret (it’s a subdomain of ours), but it’s worth knowing: every certificate you issue is a public, permanent fact.
Go and check it: look it up in the public logs
Those two SCTs aren’t a theoretical boast: you can check them yourself. An engine that aggregates the Certificate Transparency logs is crt.sh. Search the name in the browser, or query its JSON API:
# every certificate issued for the name (or for % = all subdomains)
curl 'https://crt.sh/?q=chr01test.mikrotikdojo.com&output=json' | jq .
curl 'https://crt.sh/?q=%25.mikrotikdojo.com&output=json' | jq .
The response, for each certificate found, has this shape:
{
"id": 1......, <-- id in the CT registry
"issuer_name": "C=US, O=Let's Encrypt, CN=YR1",
"name_value": "chr01test.mikrotikdojo.com",
"not_before": "2026-10-30T16:37:38",
"not_after": "2027-01-28T16:37:37"
}
Two uses, one playful and one serious. Playful: it’s proof that our experiment is now a public, permanent fact. Serious: querying %25.yourdomain you see every certificate ever issued for your domain — and if one shows up that you didn’t request, you’ve found a mis-issuance. That’s how Certificate Transparency turns a silent attack into a visible fact. (A freshly issued certificate can take from a few minutes to a few hours to appear in the aggregators.)
Step 8: the chain of trust, down to the root
A certificate doesn’t live alone: it chains up to a root your device already trusts. Let’s follow the chain by downloading each link (the address to fetch it from is in each certificate’s CA Issuers field):
chr01test.mikrotikdojo.com (leaf · DV · 90 days · CA:FALSE)
└─ issued by YR1 (intermediate · 2025→2028 · CA:TRUE pathlen:0 · EKU=TLS server only)
└─ issued by ISRG Root YR (root · 2026→2032)
└─ CROSS-SIGNED by ISRG Root X1 (the historic root, in every device)
There’s a surprise at the bottom, and it’s the nicest lesson. The root is not the famous ISRG Root X1, but a new root, “ISRG Root YR” (which appeared in 2026). How does an old device, which doesn’t know it, come to trust it? Because ISRG Root YR is cross-signed by ISRG Root X1, which has been inside every browser and operating system for years. A device that doesn’t recognize “Root YR” climbs one more step and reaches “Root X1”, which it knows — and trusts. That’s exactly how a CA introduces a new root without waiting for millions of devices to receive it: it has it signed by a root that’s already trusted. We saw it in the field, not recited from memory.
Note too that the YR1 intermediate has EKU = TLS Web Server only and pathlen:0: the middle link too is restricted to what it must do — it cannot sign other CAs, nor certificates for uses other than TLS server. The chain of trust is also a chain of restricted powers.
Step 9: putting it to use, and closing up
The certificate is there: now you use it where it’s needed. To get Webfig over real HTTPS, you assign it to the www-ssl service:
/ip service set www-ssl certificate=acme_cert_chr01test.mikrotikdojo.com disabled=no
Renewal is automatic (RouterOS redoes it before expiry), but mind this: renewal needs the challenge again, so port 80 will have to be reachable again at that moment. If you’d opened it only for issuance, plan the renewal window — or switch to the DNS-01 challenge (see the KB), which opens no ports.
And now close port 80: with the port forward removed, all the scanner traffic we saw at Step 5 disappears too. The exposed surface drops to zero. Port 80 reopens only when needed, for as long as needed.
Frequently asked questions
Can I use it without my own domain, with the MikroTik IP Cloud name?
Yes: the IP Cloud ...sn.mynetname.net name resolves to the router and works as a public name for the challenge. Keep in mind, though, that this name contains the device’s serial number in clear, and that the certificate (hence that name) will end up in the public CT logs forever. Your own domain is cleaner.
Do I really have to open port 80? Isn’t it a risk?
For the http-01 challenge, yes, port 80 inbound is needed — and we showed the risk (scanners arrive within seconds). You mitigate it by keeping it open only during the issuance/renewal window and by limiting floods by rate, not by IP. To open no port at all there’s the DNS-01 challenge (a TXT record in DNS): we cover it in the certificates KB.
Why isn’t it enough to accept only Let’s Encrypt IPs on port 80?
Because Let’s Encrypt doesn’t publish them, on purpose: it validates from many perspectives that change without notice (that’s the anti-hijack MPIC). An allow-list would break, and is discouraged by the CA itself.
The certificate lasts 90 days: do I have to remember to renew it?
No: add-acme schedules renewal on its own. Your job is to make sure that, at renewal time, the challenge condition is satisfied again (port 80 reachable, or DNS-01 configured).
Can I get an ECDSA certificate instead of RSA?
Yes, Let’s Encrypt also issues on elliptic-curve keys; you choose the key type in the command. Here we left the default (RSA 2048) for readability, but ECDSA gives smaller keys at equal strength — handy on a router. The difference between the two families is explained in the certificates KB.
Tested in the lab on CHR RouterOS 7.25 (stable), with a sniffer on the Internet-facing interface. Real issuance from Let’s Encrypt (production directory) for a test subdomain, via /certificate/add-acme with the http-01 challenge and the www service on port 80 (forwarded from the edge). The three outcomes were captured: failure “Error getting validation data” (port 80 not reachable inbound), failure “During secondary validation … Timeout during connect” (the www with max-sessions=20 saturated by the SYN flood of a scanner — 1474 attempts from a single IP within seconds of opening — cutting off one Let’s Encrypt MPIC validation perspective), and success after www max-sessions=100 with certificate import and auto-scheduled renewal. The resulting certificate was dissected with openssl: DV (policy 2.23.140.1.2.1), ~90-day validity, SAN-only identity, EKU TLS-server only, no OCSP URL (CRL only), two Certificate Transparency SCTs; chain rebuilt by downloading the links — leaf → intermediate YR1 → ISRG Root YR (2026) cross-signed by ISRG Root X1; the certificate’s presence in the Certificate Transparency logs is verifiable on crt.sh. The subdomain chr01test.mikrotikdojo.com is real (already in the CT logs); the lab’s internal IPs and paths are omitted.



