From a8caf4c37511750abcf569bad18db37e675ca52c Mon Sep 17 00:00:00 2001 From: Ilya Kulakov Date: Fri, 27 Mar 2026 12:10:13 -0700 Subject: [PATCH] plugin/tls: Add the keylog option to configure TLSConfig.KeyLogWriter (#7537) * tls: Add the keylog option to configure TLSConfig.KeyLogWriter Signed-off-by: Ilya Kulakov * tls: Close keylog file on instance shutdown. Signed-off-by: Ilya Kulakov --------- Signed-off-by: Ilya Kulakov --- plugin/tls/README.md | 4 ++++ plugin/tls/tls.go | 31 +++++++++++++++++++++++++++++++ plugin/tls/tls_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/plugin/tls/README.md b/plugin/tls/README.md index 1f42a7e0f..a1ebd04ce 100644 --- a/plugin/tls/README.md +++ b/plugin/tls/README.md @@ -27,6 +27,7 @@ Parameter CA is optional. If not set, system CAs can be used to verify the clien ~~~ txt tls CERT KEY [CA] { client_auth nocert|request|require|verify_if_given|require_and_verify + keylog FILE } ~~~ @@ -35,6 +36,9 @@ The option value corresponds to the [ClientAuthType values of the Go tls package The default is "nocert". Note that it makes no sense to specify parameter CA unless this option is set to verify\_if\_given or require\_and\_verify. +The keylog can be specified to export TLS master secrets in key log format to allow external programs +to decrypt TLS connections. It compromises security and should only be used for debugging! + ## Examples Start a DNS-over-TLS server that picks up incoming DNS-over-TLS queries on port 5553 and uses the diff --git a/plugin/tls/tls.go b/plugin/tls/tls.go index ff60b678c..9bbe7ead9 100644 --- a/plugin/tls/tls.go +++ b/plugin/tls/tls.go @@ -2,14 +2,18 @@ package tls import ( ctls "crypto/tls" + "os" "path/filepath" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/pkg/tls" ) +var log = clog.NewWithPlugin("tls") + func init() { plugin.Register("tls", setup) } func setup(c *caddy.Controller) error { @@ -33,6 +37,7 @@ func parseTLS(c *caddy.Controller) error { return plugin.Error("tls", c.ArgErr()) } clientAuth := ctls.NoClientCert + var keyLog string for c.NextBlock() { switch c.Val() { case "client_auth": @@ -54,6 +59,15 @@ func parseTLS(c *caddy.Controller) error { default: return c.Errf("unknown authentication type '%s'", authTypeArgs[0]) } + case "keylog": + args := c.RemainingArgs() + if len(args) != 1 { + return c.ArgErr() + } + keyLog = args[0] + if !filepath.IsAbs(keyLog) && config.Root != "" { + keyLog = filepath.Join(config.Root, keyLog) + } default: return c.Errf("unknown option '%s'", c.Val()) } @@ -71,6 +85,23 @@ func parseTLS(c *caddy.Controller) error { // NewTLSConfigFromArgs only sets RootCAs, so we need to let ClientCAs refer to it. tls.ClientCAs = tls.RootCAs + if len(keyLog) > 0 { + absKeyLog, err := filepath.Abs(keyLog) + if err != nil { + return c.Errf("unable to write TLS Key Log to %q: %s", keyLog, err) + } + f, err := os.OpenFile(absKeyLog, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + return c.Errf("unable to write TLS Key Log to %q: %s", absKeyLog, err) + } + c.OnShutdown(func() error { + f.Close() + return nil + }) + tls.KeyLogWriter = f + log.Warningf("Writing TLS Key Log to %q\n", absKeyLog) + } + config.TLSConfig = tls } return nil diff --git a/plugin/tls/tls_test.go b/plugin/tls/tls_test.go index 7deb837f3..921f46320 100644 --- a/plugin/tls/tls_test.go +++ b/plugin/tls/tls_test.go @@ -2,6 +2,8 @@ package tls import ( "crypto/tls" + "os" + "path/filepath" "strings" "testing" @@ -23,6 +25,7 @@ func TestTLS(t *testing.T) { {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth require\n}", false, "", ""}, {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth verify_if_given\n}", false, "", ""}, {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth require_and_verify\n}", false, "", ""}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nkeylog tls.log\n}", false, "", ""}, // negative {"tls test_cert.pem test_key.pem test_ca.pem {\nunknown\n}", true, "", "unknown option"}, // client_auth takes exactly one parameter, which must be one of known keywords. @@ -85,3 +88,39 @@ func TestTLSClientAuthentication(t *testing.T) { } } } + +func TestTLSKeyLog(t *testing.T) { + t.Run("No Path", func(t *testing.T) { + input := "tls test_cert.pem test_key.pem test_ca.pem {\nkeylog\n}" + c := caddy.NewTestController("dns", input) + err := setup(c) + if err == nil { + t.Error("Expected error but found none") + } + }) + + t.Run("Bad Path", func(t *testing.T) { + tmpDir := t.TempDir() + os.Chmod(tmpDir, 0000) + input := "tls test_cert.pem test_key.pem test_ca.pem {\nkeylog " + filepath.Join(tmpDir, "tls.log") + "\n}" + c := caddy.NewTestController("dns", input) + err := setup(c) + if err == nil { + t.Error("Expected error but found none") + } + }) + + t.Run("Good Path", func(t *testing.T) { + tmpDir := t.TempDir() + input := "tls test_cert.pem test_key.pem test_ca.pem {\nkeylog " + filepath.Join(tmpDir, "tls.log") + "\n}" + c := caddy.NewTestController("dns", input) + err := setup(c) + if err != nil { + t.Errorf("Expected no error but found %v", err) + } + cfg := dnsserver.GetConfig(c) + if cfg.TLSConfig.KeyLogWriter == nil { + t.Fatal("KeyLogWriter is not set") + } + }) +}