Connection refused?

Hi, I'm trying to get a cert for my domain and didn't plan on running a web server. I've forwarded ports 80 and 443 to the machine I'm running certbot on, but it seems the connection is refused. Is it possible the builtin webserver just isn't starting or something? Not sure how to troubleshoot this anymore..

Thanks for any help

Please fill out the fields below so we can help you better. Note: you must provide your domain name to get help. Domain names for issued certificates are all made public in Certificate Transparency logs (e.g. https://crt.sh/?q=example.com), so withholding your domain name here does not increase secrecy, but only makes it harder for us to provide help.

My domain is: smithholm.com

I ran this command: certbot certonly --standalone -d smithholm.com --dry-run

It produced this output:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for smithholm.com
Waiting for verification...
Challenge failed for domain smithholm.com
http-01 challenge for smithholm.com
Cleaning up challenges
Some challenges have failed.

IMPORTANT NOTES:

  • The following errors were reported by the server:

    Domain: smithholm.com
    Type: connection
    Detail: Fetching
    http://smithholm.com/.well-known/acme-challenge/vQppaTuO5bqeGqM5WC-GEP22ELl4N bZjtcplFq2t4mU:
    Connection refused

    To fix these errors, please make sure that your domain name was
    entered correctly and the DNS A/AAAA record(s) for that domain
    contain(s) the right IP address. Additionally, please check that
    your computer has a publicly routable IP address and that no
    firewalls are preventing the server from communicating with the
    client. If you're using the webroot plugin, you should also verify
    that you are serving files from the webroot path you provided.

My web server is (include version): N/A

The operating system my web server runs on is (include version):
HassOS
My hosting provider, if applicable, is:

I can login to a root shell on my machine (yes or no, or I don't know): yes

I'm using a control panel to manage my site (no, or provide the name and version of the control panel):
no

The version of my client is (e.g. output of certbot --version or certbot-auto --version if you're using Certbot): 1.4.0

Hi and welcome to the LE community forum.

Both ports seems to be blocked to my IP.
Let's be sure we are on the right IP.
Please show:
curl -4 ifconfig.co

It might help to (temporarily) run a webserver on port 80 yourself, and see whether it can be accessed using https://websitedown.io/smithholm.com or something:

cd $(mktemp -d) && sudo python2 -m SimpleHTTPServer 80

If that doesn't work either, then the port forwarding is probably misconfigured.

Both ports wouldn't respond right now because there's nothing listening on them. I only opened them so that the temp server spun up by certbot would work.

But they do respond:

curl -Iki http://smithholm.com/
curl: (7) Failed to connect to smithholm.com port 80: Connection refused

curl -Iki https://smithholm.com/
curl: (7) Failed to connect to smithholm.com port 443: Connection refused

Connection refused

is a (negative) response.

1 Like

I don't have python installed, but if I forward port 80 to 8123 for my homeassistant website it does come up. So my ISP is definitely not blocking port 80 either.

If you curl your own system on port 80 (internally) does it reply with connection refused?

$ curl 127.0.0.1 80
curl: (7) Failed to connect to 127.0.0.1 port 80: Connection refused

OK!
Then maybe we are talking to the same system :slight_smile:

We need some detailed logs from when certbot is running.
Try adding -v or -vv and show the logfile after it fails.

Also you might want to switch to the staging system for this test phase.

Huh, ok so I installed python..

python2 -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...

I then tried to hit it from outside my network and got connection refused!.. I also tried hitting http://192.168.0.254 (the IP of this server) and also got connection refused! So seems like for some reason the server itself is blocking http traffic... weird..

As to certbot output here it is:

certbot certonly --standalone -d smithholm.com --dry-run -vv
Root logging level set at 0
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requested authenticator standalone and installer None
Single candidate plugin: * standalone
Description: Spin up a temporary webserver
Interfaces: IAuthenticator, IPlugin
Entry point: standalone = certbot._internal.plugins.standalone:Authenticator
Initialized: <certbot._internal.plugins.standalone.Authenticator object at 0x75804448>
Prep: True
Selected authenticator <certbot._internal.plugins.standalone.Authenticator object at 0x75804448> and installer None
Plugins selected: Authenticator standalone, Installer None
Picked account: <Account(RegistrationResource(body=Registration(key=None, contact=(), agreement=None, status=None, terms_of_service_agreed=None, only_return_existing=None, external_account_binding=None), uri='https://acme-staging-v02.api.letsencrypt.org/acme/acct/17322356', new_authzr_uri=None, terms_of_service=None), 83022eca331c72ae2840968bc5537e4d, Meta(creation_dt=datetime.datetime(2020, 12, 30, 23, 53, tzinfo=), creation_host='core-ssh.local.hass.io'))>
Sending GET request to https://acme-staging-v02.api.letsencrypt.org/directory.
Starting new HTTPS connection (1): acme-staging-v02.api.letsencrypt.org:443
https://acme-staging-v02.api.letsencrypt.org:443 "GET /directory HTTP/1.1" 200 724
Received response:
HTTP 200
Server: nginx
Date: Thu, 31 Dec 2020 01:23:41 GMT
Content-Type: application/json
Content-Length: 724
Connection: keep-alive
Cache-Control: public, max-age=0, no-cache
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800

{
"JPKh5PzZ7dU": "Adding random entries to the directory",
"keyChange": "https://acme-staging-v02.api.letsencrypt.org/acme/key-change",
"meta": {
"caaIdentities": [
"letsencrypt.org"
],
"termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
"website": "https://letsencrypt.org/docs/staging-environment/"
},
"newAccount": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct",
"newNonce": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce",
"newOrder": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order",
"revokeCert": "https://acme-staging-v02.api.letsencrypt.org/acme/revoke-cert"
}
Obtaining a new certificate
Requesting fresh nonce
Sending HEAD request to https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce.
https://acme-staging-v02.api.letsencrypt.org:443 "HEAD /acme/new-nonce HTTP/1.1" 200 0
Received response:
HTTP 200
Server: nginx
Date: Thu, 31 Dec 2020 01:23:44 GMT
Connection: keep-alive
Cache-Control: public, max-age=0, no-cache
Link: https://acme-staging-v02.api.letsencrypt.org/directory;rel="index"
Replay-Nonce: 0003b8_w0pbO3clZ8-EQmEazz0Fsk-kTaRPfwFtTBQt_xWE
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800

Storing nonce: 0003b8_w0pbO3clZ8-EQmEazz0Fsk-kTaRPfwFtTBQt_xWE
JWS payload:
b'{\n "identifiers": [\n {\n "type": "dns",\n "value": "smithholm.com"\n }\n ]\n}'
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/new-order:
{
"protected": "eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvYWNjdC8xNzMyMjM1NiIsICJub25jZSI6ICIwMDAzYjhfdzBwYk8zY2xaOC1FUW1FYXp6MEZzay1rVGFSUGZ3RnRUQlF0X3hXRSIsICJ1cmwiOiAiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIifQ",
"signature": "Itcd7MS-MVM_YaQKwdKL9m6SANufnGHsU1Nx0cpNrz7JfoDeZe6ruE7K8xU8fq6032jBmX9yl9QPf4_6tNrQU09xvbn-Fh73MTaGP7QJf098EMTMVmlBKKigAWoaNMhpS4YAl96MWCIJuXQ5U3gYJMHG1U1l-LbR1Q_ptEl8M6lJ4URiJ-lZoTEWE9Jj75SEm_fRX1ctlma78gW51PvD49XGFjLF4Hb3Mwc8ALqV_YKPBx2O8Edw8WGjZaGpw2XMHcC_F0LoGYFos4YIp5rHQ6GI7HdGKqRvfyrSBBp3uCrza1_Jxg2bMCHpv38DmD8a5HKccZ5p5E61_WafpuDa-A",
"payload": "ewogICJpZGVudGlmaWVycyI6IFsKICAgIHsKICAgICAgInR5cGUiOiAiZG5zIiwKICAgICAgInZhbHVlIjogInNtaXRoaG9sbS5jb20iCiAgICB9CiAgXQp9"
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/new-order HTTP/1.1" 201 347
Received response:
HTTP 201
Server: nginx
Date: Thu, 31 Dec 2020 01:23:44 GMT
Content-Type: application/json
Content-Length: 347
Connection: keep-alive
Boulder-Requester: 17322356
Cache-Control: public, max-age=0, no-cache
Link: https://acme-staging-v02.api.letsencrypt.org/directory;rel="index"
Location: https://acme-staging-v02.api.letsencrypt.org/acme/order/17322356/210511488
Replay-Nonce: 0004sTflsAwZLgEkZuN3FY33Y_R5d39rMTcEmsAkA-zwTr8
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800

{
"status": "pending",
"expires": "2021-01-07T01:23:44Z",
"identifiers": [
{
"type": "dns",
"value": "smithholm.com"
}
],
"authorizations": [
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/181497081"
],
"finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/17322356/210511488"
}
Storing nonce: 0004sTflsAwZLgEkZuN3FY33Y_R5d39rMTcEmsAkA-zwTr8
JWS payload:
b''
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/181497081:
{
"protected": "eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvYWNjdC8xNzMyMjM1NiIsICJub25jZSI6ICIwMDA0c1RmbHNBd1pMZ0VrWnVOM0ZZMzNZX1I1ZDM5ck1UY0Vtc0FrQS16d1RyOCIsICJ1cmwiOiAiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xODE0OTcwODEifQ",
"signature": "fDXPQ7izkfh1rjzSbBzgR-plchNngirltsky0_ys0UlGmcQ9JcDfegBtd5pzkuenB8c09rMcCoINo9irdFF-YiRkiBSxg3ACBLcs5CpDeKdw-w4z1RzTCuYnVuXDj4jqaVTWVJ7vEHRT07nanaEmgYDP8p-eQsXuL9sq8pSoiwLfiFjXn_BMeBPw0PeI088r4LBnyoB6Chu-2OMUmb5pvysDvA5-AZM2bEcLsMiFy-NzIjrL1dUrKndXSdYeD_5Q1YIIXkSZjAkIollcNDTsEJI3_9dIoEjhlGnJSBfC-nMPj1e_b0hGIUJL3o8tiim6iEVCzIf2HCydj2y-hzJztw",
"payload": ""
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/authz-v3/181497081 HTTP/1.1" 200 812
Received response:
HTTP 200
Server: nginx
Date: Thu, 31 Dec 2020 01:23:44 GMT
Content-Type: application/json
Content-Length: 812
Connection: keep-alive
Boulder-Requester: 17322356
Cache-Control: public, max-age=0, no-cache
Link: https://acme-staging-v02.api.letsencrypt.org/directory;rel="index"
Replay-Nonce: 0004moCSnZDVHobQB1jvJlPnXGBj7S9y1ypkmk5RTu36DaU
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800

{
"identifier": {
"type": "dns",
"value": "smithholm.com"
},
"status": "pending",
"expires": "2021-01-07T01:23:44Z",
"challenges": [
{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/LereyA",
"token": "dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/6e27gA",
"token": "dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ"
},
{
"type": "tls-alpn-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/5PCW6A",
"token": "dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ"
}
]
}
Storing nonce: 0004moCSnZDVHobQB1jvJlPnXGBj7S9y1ypkmk5RTu36DaU
Performing the following challenges:
http-01 challenge for smithholm.com
Successfully bound to :80 using IPv6
Certbot wasn't able to bind to :80 using IPv4, this is often expected due to the dual stack nature of IPv6 socket implementations.
Waiting for verification...
JWS payload:
b'{}'
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/LereyA:
{
"protected": "eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvYWNjdC8xNzMyMjM1NiIsICJub25jZSI6ICIwMDA0bW9DU25aRFZIb2JRQjFqdkpsUG5YR0JqN1M5eTF5cGttazVSVHUzNkRhVSIsICJ1cmwiOiAiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9jaGFsbC12My8xODE0OTcwODEvTGVyZXlBIn0",
"signature": "e6pM1Ei0c1y5jTX66p1O8wbOcoPl4a2DiVAeNcqmycglPFqh4XtkWDjLFxyl6x5tt7LFBD8in0bM_qlbbfJZOKKpcJ0adtIUzGSKIfgRahvWMvZtVTUZriuNnearXRxX__VB5Hc1VadeEc-U5tU1c_wXqYwPmru5epk-qngHKGpIE6ZJYyKNYx_onvy37AiV0zCroYCPA2uazUj6K95S3uIvbDDh9gFHgc-OOaLJ0CYMpJ03FKm-Pdx23e-Wg4oFuRwrSDZXaJmlsuase8aC0e38uW51BWP2UPpay9S7Y7rwb451rzmZDxEOCH51tvsWwJ5PmjxSyqryITI-1f8t1g",
"payload": "e30"
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/chall-v3/181497081/LereyA HTTP/1.1" 200 192
Received response:
HTTP 200
Server: nginx
Date: Thu, 31 Dec 2020 01:23:44 GMT
Content-Type: application/json
Content-Length: 192
Connection: keep-alive
Boulder-Requester: 17322356
Cache-Control: public, max-age=0, no-cache
Link: https://acme-staging-v02.api.letsencrypt.org/directory;rel="index", https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/181497081;rel="up"
Location: https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/LereyA
Replay-Nonce: 0003CZ0RBX5bo2jjIg_UM4VzVgDzyaKdBiMwh6Z6CyIQSGQ
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800

{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/LereyA",
"token": "dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ"
}
Storing nonce: 0003CZ0RBX5bo2jjIg_UM4VzVgDzyaKdBiMwh6Z6CyIQSGQ
JWS payload:
b''
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/181497081:
{
"protected": "eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImh0dHBzOi8vYWNtZS1zdGFnaW5nLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvYWNjdC8xNzMyMjM1NiIsICJub25jZSI6ICIwMDAzQ1owUkJYNWJvMmpqSWdfVU00VnpWZ0R6eWFLZEJpTXdoNlo2Q3lJUVNHUSIsICJ1cmwiOiAiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei12My8xODE0OTcwODEifQ",
"signature": "Ws2eZYKNKWUqSsA6yKCf2sWiGYRKUnCru9TqP-CCYZY2jkjAxeoqccnvWejdZ5jV6a5bfn456X3yLGU-_J_jcSiMbMgzkRX-ux7IJeGAkgyF4__j41DX66WfoVTvT0ZMO1PhzPSCn_tSZ-hKUfAbPFI9EGnzraGvdxuHaNKO1hxbaQ6rdKGQdxIAi22jAQPQVrm65m5O3SiA-dTagOT0T09hn4gxC4Zfi4iTnRk4ZFkvUiQZjtMVrRTWGFnHM5YSp7UKVshkaPZzXgHVN06_ZOnyHbcRsJ23iqip1uVT7mQvvvaf4qb2F-Zuoc9d6vOLDE0N1U4_G2I3YEgfnrqRYg",
"payload": ""
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/authz-v3/181497081 HTTP/1.1" 200 967
Received response:
HTTP 200
Server: nginx
Date: Thu, 31 Dec 2020 01:23:45 GMT
Content-Type: application/json
Content-Length: 967
Connection: keep-alive
Boulder-Requester: 17322356
Cache-Control: public, max-age=0, no-cache
Link: https://acme-staging-v02.api.letsencrypt.org/directory;rel="index"
Replay-Nonce: 00036sahF0RseQBUbd56MZiPjVyZdDIrtaIXCYV-Oe8YxA4
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800

{
"identifier": {
"type": "dns",
"value": "smithholm.com"
},
"status": "invalid",
"expires": "2021-01-07T01:23:44Z",
"challenges": [
{
"type": "http-01",
"status": "invalid",
"error": {
"type": "urn:ietf:params:acme:error:connection",
"detail": "Fetching http://smithholm.com/.well-known/acme-challenge/dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ: Connection refused",
"status": 400
},
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/181497081/LereyA",
"token": "dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ",
"validationRecord": [
{
"url": "http://smithholm.com/.well-known/acme-challenge/dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ",
"hostname": "smithholm.com",
"port": "80",
"addressesResolved": [
"45.72.252.109"
],
"addressUsed": "45.72.252.109"
}
]
}
]
}
Storing nonce: 00036sahF0RseQBUbd56MZiPjVyZdDIrtaIXCYV-Oe8YxA4
Challenge failed for domain smithholm.com
http-01 challenge for smithholm.com
Reporting to user: The following errors were reported by the server:

Domain: smithholm.com
Type: connection
Detail: Fetching http://smithholm.com/.well-known/acme-challenge/dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ: Connection refused

To fix these errors, please make sure that your domain name was entered correctly and the DNS A/AAAA record(s) for that domain contain(s) the right IP address. Additionally, please check that your computer has a publicly routable IP address and that no firewalls are preventing the server from communicating with the client. If you're using the webroot plugin, you should also verify that you are serving files from the webroot path you provided.
Encountered exception:
Traceback (most recent call last):
File "/usr/lib/python3.8/site-packages/certbot/_internal/auth_handler.py", line 91, in handle_authorizations
self._poll_authorizations(authzrs, max_retries, best_effort)
File "/usr/lib/python3.8/site-packages/certbot/_internal/auth_handler.py", line 180, in _poll_authorizations
raise errors.AuthorizationError('Some challenges have failed.')
certbot.errors.AuthorizationError: Some challenges have failed.

Calling registered functions
Cleaning up challenges
Stopping server at :::80...
Exiting abnormally:
Traceback (most recent call last):
File "/usr/bin/certbot", line 11, in
load_entry_point('certbot==1.4.0', 'console_scripts', 'certbot')()
File "/usr/lib/python3.8/site-packages/certbot/main.py", line 15, in main
return internal_main.main(cli_args)
File "/usr/lib/python3.8/site-packages/certbot/_internal/main.py", line 1347, in main
return config.func(config, plugins)
File "/usr/lib/python3.8/site-packages/certbot/_internal/main.py", line 1233, in certonly
lineage = _get_and_save_cert(le_client, config, domains, certname, lineage)
File "/usr/lib/python3.8/site-packages/certbot/_internal/main.py", line 121, in _get_and_save_cert
lineage = le_client.obtain_and_enroll_certificate(domains, certname)
File "/usr/lib/python3.8/site-packages/certbot/_internal/client.py", line 409, in obtain_and_enroll_certificate
cert, chain, key, _ = self.obtain_certificate(domains)
File "/usr/lib/python3.8/site-packages/certbot/_internal/client.py", line 343, in obtain_certificate
orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)
File "/usr/lib/python3.8/site-packages/certbot/_internal/client.py", line 390, in _get_order_and_authorizations
authzr = self.auth_handler.handle_authorizations(orderr, best_effort)
File "/usr/lib/python3.8/site-packages/certbot/_internal/auth_handler.py", line 91, in handle_authorizations
self._poll_authorizations(authzrs, max_retries, best_effort)
File "/usr/lib/python3.8/site-packages/certbot/_internal/auth_handler.py", line 180, in _poll_authorizations
raise errors.AuthorizationError('Some challenges have failed.')
certbot.errors.AuthorizationError: Some challenges have failed.
Some challenges have failed.

IMPORTANT NOTES:

  • The following errors were reported by the server:

    Domain: smithholm.com
    Type: connection
    Detail: Fetching
    http://smithholm.com/.well-known/acme-challenge/dDIMs_LsDVo5O0rhhWVW5LxutgM5Ak-m95lrtySavJQ:
    Connection refused

    To fix these errors, please make sure that your domain name was
    entered correctly and the DNS A/AAAA record(s) for that domain
    contain(s) the right IP address. Additionally, please check that
    your computer has a publicly routable IP address and that no
    firewalls are preventing the server from communicating with the
    client. If you're using the webroot plugin, you should also verify
    that you are serving files from the webroot path you provided.

1 Like

Try running the python webserver with some high unused port - like 20000
then hit it internally, does it still say connection refused?

1 Like

You can also port forward 80 -> 20000 and then:

certbot certonly --standalone --http-01-port 20000

which will have the same effect.

1 Like

If we could just get past the connection refused!

iptables --list
sudo ufw status

python2 -m SimpleHTTPServer 20000
Serving HTTP on 0.0.0.0 port 20000 ...

Still connection refused when navigating to http://192.168.0.254:20000 in the browser.

Incidentally I also tried opening another ssh session to the box, and did this while the python server was running:

telnet localhost 20000
telnet: can't connect to remote host (127.0.0.1): Connection refused

Also, I noticed python says "Serving HTTP on 0.0.0.0 port 20000". Obviously 0.0.0.0 isn't a valid ip.. Anything to be concerned about there? Maybe it's not really listening?

0.0.0.0 means ALL local IPs.
Try running this while python is up.
sudo netstat -pant | grep -i listen

Wait I lied.. I must've killed the python server before I tried telnetting.. telnet does work..

On 20000?
But NOT 80?

We'll take what we can get.
port forward ext:80 to internal:20000
Then
certbot certonly --standalone --http-01-port 20000 --staging

Perfect:
tcp 0 0 0.0.0.0:20000 0.0.0.0:* LISTEN 752/python2
shut that down and use certbot on that port

tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 448/sshd -D -e [lis
tcp 0 0 0.0.0.0:20000 0.0.0.0:* LISTEN 752/python2
tcp 0 0 0.0.0.0:8099 0.0.0.0:* LISTEN 447/ttyd
tcp 0 0 127.0.0.11:42951 0.0.0.0:* LISTEN -
tcp 0 0 :::22 :::* LISTEN 448/sshd -D -e [lis