Custom chat certificate setup for KebsCS/LeagueClientDebugger
By default the debugger downloads its TLS certificate from pfx.lolcert.online and points the Riot client at localhost.lolcert.online. That just works — no setup needed.
If you'd rather host your own certificate (because you don't trust the default infrastructure, want to control renewal, or are running this in a place where lolcert.online is blocked), follow this guide.
- A domain you control (cheap
.xyzfrom Namecheap, Porkbun, etc. — around $1-2/year). - A free Cloudflare account.
- A place to host a small file over HTTPS. This guide uses Cloudflare R2 (free tier, no egress cost), but any static hosting works (GitHub Pages, Backblaze B2, S3, your own VPS).
- A GitHub account (for the renewal workflow).
- A public DNS A record points
localhost.your-domain.example.comto127.0.0.1. When the Riot client tries to resolve that hostname it gets127.0.0.1, which is your own machine. - A real Let's Encrypt certificate is issued for that hostname using the DNS-01 challenge (since the hostname doesn't point to a real server, HTTP-01 won't work).
- The certificate is exported as a passwordless
.pfxand uploaded to a public HTTPS URL. - The debugger downloads that
.pfx, caches it locally, and uses it to serve TLS on its local chat proxy. - The Riot client connects to
localhost.your-domain.example.com:5223, gets127.0.0.1, performs a TLS handshake against a valid cert, and the proxy intercepts the chat traffic.
A GitHub Actions workflow re-issues the cert monthly and re-uploads it. Let's Encrypt certs are valid for 90 days, so monthly renewal leaves plenty of margin.
Register any cheap domain. Skip .xyz if you want — .click, .online, .site, anything works. You won't be hosting a website on it.
- Sign up at cloudflare.com (free).
- Dashboard → Add a site → enter your domain → Continue.
- Pick the Free plan → Continue.
- Cloudflare scans existing DNS records → Continue to activation.
- Cloudflare gives you two nameservers, like:
Copy them.
blake.ns.cloudflare.com lia.ns.cloudflare.com - Go to your registrar (Namecheap, Porkbun, etc.) → dashboard → your domain → Nameservers → switch to Custom DNS → paste Cloudflare's two nameservers → save.
- Wait for Cloudflare to email you confirming the domain is active. Usually 10-30 minutes.
In Cloudflare, with your domain active:
- DNS → Records → Add record:
| Field | Value |
|---|---|
| Type | A |
| Name | localhost |
| IPv4 address | 127.0.0.1 |
| Proxy status | DNS only (gray cloud, not orange) |
| TTL | Auto |
Save. Verify from a terminal:
nslookup localhost.your-domain.example.com
Should return 127.0.0.1. If not, wait a couple of minutes and retry.
This is used by the GitHub Actions workflow to add the DNS-01 challenge record.
- Cloudflare → top right user icon → My Profile → API Tokens → Create Token.
- Use the Edit zone DNS template.
- Zone Resources: pick your specific domain (not "all zones").
- Continue to summary → Create Token.
- Copy the token (only shown once).
While you're there, grab the Zone ID of your domain from Cloudflare → your domain dashboard → right sidebar.
This is where the .pfx will live.
- Cloudflare → left sidebar → R2 Object Storage.
- Accept the terms (you'll need to add a payment method, but you won't be charged within the free tier).
- Create bucket → name it
cert(or whatever) → Create bucket. - Open the bucket → Settings tab → Custom Domains → Connect Domain.
- Domain:
pfx.your-domain.example.com(any subdomain works). - Continue — Cloudflare creates the DNS record automatically. Wait a minute for it to show as Active.
Then create an R2 API token:
- Sidebar → R2 Object Storage → API → Manage API tokens → Create Account API token.
- Token name: anything (
cert-uploader). - Permissions: Object Read and Write.
- Apply to specific buckets only → select your bucket.
- Create.
- Copy the Access Key ID, Secret Access Key, and the Account ID (visible in the S3 API URL, e.g.
https://<account-id>.r2.cloudflarestorage.com). Only shown once.
- Create a new GitHub repository. Add a README so it's not empty.
- Settings → Secrets and variables → Actions → New repository secret. Add five secrets:
| Name | Value |
|---|---|
CF_API_TOKEN |
Cloudflare DNS token from step 4 |
CF_ZONE_ID |
Zone ID from step 4 |
R2_ACCESS_KEY_ID |
From step 5 |
R2_SECRET_ACCESS_KEY |
From step 5 |
R2_ACCOUNT_ID |
From step 5 |
- Create
.github/workflows/renew.ymlin the repo:
name: Renew certificate
on:
schedule:
- cron: '0 6 1 * *'
workflow_dispatch:
jobs:
renew:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install acme.sh
run: curl https://get.acme.sh | sh -s email=YOUR@EMAIL.COM
- name: Issue cert with DNS-01 via Cloudflare
env:
CF_Token: ${{ secrets.CF_API_TOKEN }}
CF_Zone_ID: ${{ secrets.CF_ZONE_ID }}
run: |
~/.acme.sh/acme.sh --issue \
--dns dns_cf \
-d "localhost.your-domain.example.com" \
--server letsencrypt
- name: Export as PFX (no password)
run: |
CERT_DIR=$(find ~/.acme.sh/ -maxdepth 1 -type d -name "localhost.your-domain.example.com*" | head -1)
openssl pkcs12 -export \
-in "$CERT_DIR/fullchain.cer" \
-inkey "$CERT_DIR/localhost.your-domain.example.com.key" \
-out localhost.pfx \
-passout pass:
- name: Upload PFX to R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
run: |
aws s3 cp localhost.pfx s3://cert/localhost.pfx \
--endpoint-url https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com \
--content-type application/x-pkcs12Replace YOUR@EMAIL.COM, localhost.your-domain.example.com, and cert (the bucket name) with your values. Commit.
- Go to Actions → Renew certificate → Run workflow → Run workflow. Wait about a minute. Should turn green.
curl -L -o test.pfx https://pfx.your-domain.example.com/localhost.pfx
openssl pkcs12 -info -noout -in test.pfx -passin pass:
The openssl call should list certificates without asking for a password. If you don't have openssl installed, just check that test.pfx is a few KB binary file.
You have two ways to override the debugger's defaults. Pick whichever you prefer.
Create a file called chat_cert_config.json next to ChatCert.py (same directory) with:
{
"pfx_url": "https://pfx.your-domain.example.com/localhost.pfx",
"chat_proxy_host": "localhost.your-domain.example.com"
}Set before running the debugger:
set LCD_PFX_URL=https://pfx.your-domain.example.com/localhost.pfx
set LCD_CHAT_PROXY_HOST=localhost.your-domain.example.com
python main.py
On Linux/macOS use export instead of set.
Environment variables override the JSON file if both are set.
The debugger caches the downloaded PFX in ~/.lcd_chat/. After switching to your own infrastructure, clear it once so the new cert gets fetched:
rmdir /s /q "%USERPROFILE%\.lcd_chat"
(Linux/macOS: rm -rf ~/.lcd_chat)
Close Riot Client completely (including the tray icon), then start the debugger. Console should show:
[ChatCert] Downloading certificate from https://pfx.your-domain.example.com/localhost.pfx
[ChatCert] Chain with 2 certificates
[ChatCert] Certificate cached at ...
[XMPP] Proxy server started on 127.0.0.1:XXXXX (TLS)
[XMPP] League client connected to proxy (...)
[XMPP] Client connected to real riot server (...)
Chat should load normally in League — friends list, presence, messages, the works.
HTTP 403 Forbidden when downloading the PFX. The CDN in front of your storage is blocking the request based on User-Agent. ChatCert.py already sends a real browser User-Agent so this shouldn't happen with R2 or GitHub Pages, but some providers are stricter. Try a different host.
WinError 64: The specified network name is no longer available. The TLS handshake is failing because intermediate certificates are missing. Confirm the workflow used acme.sh's fullchain.cer (not just <domain>.cer) when building the PFX. The example workflow above already does this correctly.
nslookup returns something other than 127.0.0.1. Your DNS hasn't propagated yet, or the registrar still points to its own nameservers. Wait. Verify the nameservers at the registrar.
Certificate expired. The workflow runs once a month on the 1st. If you set it up between runs, trigger it manually with Run workflow to get the initial cert.