Repository: ipfs/kubo

Issue:
Enabling AutoTLS feature by default
> Contributing towards https://github.com/ipshipyard/roadmaps/issues/15, https://github.com/ipshipyard/roadmaps/issues/9, https://github.com/ipshipyard/roadmaps/issues/4, https://github.com/ipshipyard/roadmaps/issues/5

## Context

In https://github.com/ipfs/kubo/pull/10521 we've introduced opt-in [`AutoTLS` feature](https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls) which uses DNS and [p2p-forge](https://github.com/ipshipyard/p2p-forge) infrastructure run by [Interplanetary Shipyard](https://ipshipyard.com/) to automate TLS setup for [libp2p WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) transport.

This feature will not yield the maximum benefit to the swarm unless it is enabled by default, just like `/webtransport` and `/webrtc-direct` listeners are.

This issue tracks remaining work towards enabling AutoTLS by default.

## TODO

- [x] https://github.com/ipfs/kubo/pull/10521 (landed in kubo 0.32.0-rc1)
- [x] `libp2p.direct` public good infra accepts registrations 
- [x] https://github.com/ipfs/kubo/pull/10566
- [ ] disable `AutoTLS` in `test` profile
- [x] https://github.com/ipfs/kubo/pull/10565 
  - [x] `AutoTLS.AutoWSS` flag that adds catch-all listener `/tcp/400X/tls/sni/*.libp2p.direct/ws` if  `Addresses.*` (swarm, announce, appendAnnounce) have no `/tls/sni` or `/wss` ()
  - [x] `/ws` listener can share the same port as `/tcp` (4001)
    - [ ] https://github.com/libp2p/go-libp2p/pull/2984
- [x] https://github.com/ipfs/kubo/issues/10570
  - [x] wait for libp2p reachability to be Public: https://github.com/ipshipyard/p2p-forge/pull/4
  - [x] do not attempt getting cert if we depend on `/p2p-circuit` addrs
  - [ ] do not attempt getting cert if the only "public addr" is ipv6 blocked by firewall:  https://github.com/ipshipyard/p2p-forge/issues/7
- [ ] Decide when `AutoTLS.Enabled` is `true` by default
  - [ ] we want to avoid noise due to CI and testing nodes 
    - 💡 idea: if `AutoTLS.Enabled` is not explicitly set in config, delay requesting cert by 1h?
  - [ ] set `AutoTLS.Enabled=false` in `test` profile 
- [ ] Agree if we use `/ipX` or `/dnsX` (https://github.com/ipshipyard/p2p-forge/pull/34#discussion_r1908966798)
- [ ] Kubo itself needs some sort of smoke-test E2E `test/cli` that gets cert from localhost `AutoTLS.CAEndpoint` and confirms it is placed in `IPFS_PATH/p2p-forge-certs/certificates/`
- [ ] `libp2p.direct` [productization by Shipyard](https://www.notion.so/libp2p-direct-2024Q3-infrastructure-revamp-proposal-12f1def342878052890feec3e21b42dc) is finished
- [x] https://github.com/libp2p/specs/pull/639



Discussion:

Solution patch:
diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go
index 0615b26d3..fe01501ec 100644
--- a/cmd/ipfs/kubo/daemon.go
+++ b/cmd/ipfs/kubo/daemon.go
@@ -421,9 +421,16 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
 		// Private setups should not use public AutoTLS infrastructure
 		// as it will leak their existence and PeerID identity to CA
 		// and they will show up at https://crt.sh/?q=libp2p.direct
-		// Below ensures we hard fail if someone tries to enable both
-		if cfg.AutoTLS.Enabled.WithDefault(config.DefaultAutoTLSEnabled) {
-			return errors.New("private networking (swarm.key / LIBP2P_FORCE_PNET) does not work with AutoTLS.Enabled=true, update config to remove this message")
+		enableAutoTLS := cfg.AutoTLS.Enabled.WithDefault(config.DefaultAutoTLSEnabled)
+		if enableAutoTLS {
+			if cfg.AutoTLS.Enabled != config.Default {
+				// hard fail if someone tries to explicitly enable both
+				return errors.New("private networking (swarm.key / LIBP2P_FORCE_PNET) does not work with AutoTLS.Enabled=true, update config to remove this message")
+			} else {
+				// print error and disable autotls if user runs on default settings
+				log.Error("private networking (swarm.key / LIBP2P_FORCE_PNET) is not compatible with AutoTLS. Set AutoTLS.Enabled=false in config to remove this message.")
+				cfg.AutoTLS.Enabled = config.False
+			}
 		}
 	}
 
diff --git a/config/autotls.go b/config/autotls.go
index 7c935fc64..805a9ded6 100644
--- a/config/autotls.go
+++ b/config/autotls.go
@@ -1,6 +1,10 @@
 package config
 
-import p2pforge "github.com/ipshipyard/p2p-forge/client"
+import (
+	"time"
+
+	p2pforge "github.com/ipshipyard/p2p-forge/client"
+)
 
 // AutoTLS includes optional configuration of p2p-forge client of service
 // for obtaining a domain and TLS certificate to improve connectivity for web
@@ -21,6 +25,9 @@ type AutoTLS struct {
 	// Optional Authorization token, used with private/test instances of p2p-forge
 	RegistrationToken *OptionalString `json:",omitempty"`
 
+	// Optional registration delay used when AutoTLS.Enabled is not explicitly set to true in config
+	RegistrationDelay *OptionalDuration `json:",omitempty"`
+
 	// Optional override of CA ACME API used by p2p-forge system
 	CAEndpoint *OptionalString `json:",omitempty"`
 
@@ -29,10 +36,11 @@ type AutoTLS struct {
 }
 
 const (
-	DefaultAutoTLSEnabled       = false // experimental, opt-in for now (https://github.com/ipfs/kubo/pull/10521)
-	DefaultDomainSuffix         = p2pforge.DefaultForgeDomain
-	DefaultRegistrationEndpoint = p2pforge.DefaultForgeEndpoint
-	DefaultCAEndpoint           = p2pforge.DefaultCAEndpoint
-	DefaultAutoWSS              = true // requires AutoTLS.Enabled
-	DefaultAutoTLSShortAddrs    = true // requires AutoTLS.Enabled
+	DefaultAutoTLSEnabled           = true // with DefaultAutoTLSRegistrationDelay, unless explicitly enabled  in config
+	DefaultDomainSuffix             = p2pforge.DefaultForgeDomain
+	DefaultRegistrationEndpoint     = p2pforge.DefaultForgeEndpoint
+	DefaultCAEndpoint               = p2pforge.DefaultCAEndpoint
+	DefaultAutoWSS                  = true // requires AutoTLS.Enabled
+	DefaultAutoTLSShortAddrs        = true // requires AutoTLS.Enabled
+	DefaultAutoTLSRegistrationDelay = 1 * time.Hour
 )
diff --git a/config/profile.go b/config/profile.go
index 03e62172a..69aaf66dc 100644
--- a/config/profile.go
+++ b/config/profile.go
@@ -86,6 +86,7 @@ is useful when using the daemon in test environments.`,
 
 			c.Bootstrap = []string{}
 			c.Discovery.MDNS.Enabled = false
+			c.AutoTLS.Enabled = False
 			return nil
 		},
 	},
@@ -104,6 +105,7 @@ Inverse profile of the test profile.`,
 
 			c.Swarm.DisableNatPortMap = false
 			c.Discovery.MDNS.Enabled = true
+			c.AutoTLS.Enabled = Default
 			return nil
 		},
 	},
diff --git a/core/node/groups.go b/core/node/groups.go
index 0f65230dd..e8f9739c3 100644
--- a/core/node/groups.go
+++ b/core/node/groups.go
@@ -112,6 +112,8 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
 		autonat = fx.Provide(libp2p.AutoNATService(cfg.AutoNAT.Throttle, true))
 	}
 
+	enableTCPTransport := cfg.Swarm.Transports.Network.TCP.WithDefault(true)
+	enableWebsocketTransport := cfg.Swarm.Transports.Network.Websocket.WithDefault(true)
 	enableRelayTransport := cfg.Swarm.Transports.Network.Relay.WithDefault(true) // nolint
 	enableRelayService := cfg.Swarm.RelayService.Enabled.WithDefault(enableRelayTransport)
 	enableRelayClient := cfg.Swarm.RelayClient.Enabled.WithDefault(enableRelayTransport)
@@ -129,14 +131,8 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
 		}
 	}
 
-	if enableAutoTLS {
-		if !cfg.Swarm.Transports.Network.TCP.WithDefault(true) {
-			logger.Fatal("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.TCP to be true as well.")
-		}
-		if !cfg.Swarm.Transports.Network.Websocket.WithDefault(true) {
-			logger.Fatal("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.Websocket to be true as well.")
-		}
-
+	switch {
+	case enableAutoTLS && enableTCPTransport && enableWebsocketTransport:
 		// AutoTLS for Secure WebSockets: ensure WSS listeners are in place (manual or automatic)
 		wssWildcard := fmt.Sprintf("/tls/sni/*.%s/ws", cfg.AutoTLS.DomainSuffix.WithDefault(config.DefaultDomainSuffix))
 		wssWildcardPresent := false
@@ -170,7 +166,8 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
 		// if no manual /ws listener was set by the user
 		if enableAutoWSS && !wssWildcardPresent && !customWsPresent {
 			if len(tcpListeners) == 0 {
-				logger.Fatal("Invalid configuration: AutoTLS.AutoWSS=true requires at least one /tcp listener present in Addresses.Swarm, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls")
+				logger.Error("Invalid configuration, AutoTLS will be disabled: AutoTLS.AutoWSS=true requires at least one /tcp listener present in Addresses.Swarm, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls")
+				enableAutoTLS = false
 			}
 			for _, tcpListener := range tcpListeners {
 				wssListener := tcpListener + wssWildcard
@@ -180,8 +177,15 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
 		}
 
 		if !wssWildcardPresent && !enableAutoWSS {
-			logger.Fatal(fmt.Sprintf("Invalid configuration: AutoTLS.Enabled=true requires a /tcp listener ending with %q to be present in Addresses.Swarm or AutoTLS.AutoWSS=true, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls", wssWildcard))
+			logger.Error(fmt.Sprintf("Invalid configuration, AutoTLS will be disabled: AutoTLS.Enabled=true requires a /tcp listener ending with %q to be present in Addresses.Swarm or AutoTLS.AutoWSS=true, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls", wssWildcard))
+			enableAutoTLS = false
 		}
+	case enableAutoTLS && !enableTCPTransport:
+		logger.Error("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.TCP to be true as well. AutoTLS will be disabled.")
+		enableAutoTLS = false
+	case enableAutoTLS && !enableWebsocketTransport:
+		logger.Error("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.Websocket to be true as well. AutoTLS will be disabled.")
+		enableAutoTLS = false
 	}
 
 	// Gather all the options
diff --git a/core/node/libp2p/addrs.go b/core/node/libp2p/addrs.go
index 4eee6a00b..8cc5287ec 100644
--- a/core/node/libp2p/addrs.go
+++ b/core/node/libp2p/addrs.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"time"
 
 	logging "github.com/ipfs/go-log"
 	version "github.com/ipfs/kubo"
@@ -145,11 +146,19 @@ func P2PForgeCertMgr(repoPath string, cfg config.AutoTLS, atlsLog *logging.ZapEv
 		certmagic.Default.Logger = rawLogger.Named("default_fixme")
 		certmagic.DefaultACME.Logger = rawLogger.Named("default_acme_client_fixme")
 
+		registrationDelay := cfg.RegistrationDelay.WithDefault(config.DefaultAutoTLSRegistrationDelay)
+		if cfg.Enabled == config.True && cfg.RegistrationDelay.IsDefault() {
+			// Skip delay if user explicitly enabled AutoTLS.Enabled in config
+			// and did not set custom AutoTLS.RegistrationDelay
+			registrationDelay = 0 * time.Second
+		}
+
 		certStorage := &certmagic.FileStorage{Path: storagePath}
 		certMgr, err := p2pforge.NewP2PForgeCertMgr(
 			p2pforge.WithLogger(rawLogger.Sugar()),
 			p2pforge.WithForgeDomain(cfg.DomainSuffix.WithDefault(config.DefaultDomainSuffix)),
 			p2pforge.WithForgeRegistrationEndpoint(cfg.RegistrationEndpoint.WithDefault(config.DefaultRegistrationEndpoint)),
+			p2pforge.WithRegistrationDelay(registrationDelay),
 			p2pforge.WithCAEndpoint(cfg.CAEndpoint.WithDefault(config.DefaultCAEndpoint)),
 			p2pforge.WithForgeAuth(cfg.RegistrationToken.WithDefault(os.Getenv(p2pforge.ForgeAuthEnv))),
 			p2pforge.WithUserAgent(version.GetUserAgentVersion()),
diff --git a/docs/changelogs/v0.34.md b/docs/changelogs/v0.34.md
index 28b6ecd25..5981ef7be 100644
--- a/docs/changelogs/v0.34.md
+++ b/docs/changelogs/v0.34.md
@@ -6,17 +6,40 @@
 
 - [Overview](#overview)
 - [🔦 Highlights](#-highlights)
+  - [AutoTLS now enabled by default for nodes with 1 hour uptime](#autotls-now-enabled-by-default-for-nodes-with-1-hour-uptime)
   - [RPC and CLI command changes](#rpc-and-cli-command-changes)
   - [Bitswap improvements from Boxo](#bitswap-improvements-from-boxo)
-  - [IPFS_LOG_LEVEL deprecated](#ipfs_log_level-deprecated)
-  - [Pebble datastore format upgrade](#pebble_datastore_format_update)
-  - [Badger datastore update](#badger_datastore_update)
+  - [`IPFS_LOG_LEVEL` deprecated](#ipfs_log_level-deprecated)
+  - [Pebble datastore format update](#pebble-datastore-format-update)
+  - [Badger datastore update](#badger-datastore-update)
 - [👨‍👩‍👧‍👦 Contributors](#-contributors)
 
 ### Overview
 
 ### 🔦 Highlights
 
+#### AutoTLS now enabled by default for nodes with 1 hour uptime
+
+Starting now, any publicly dialable Kubo node with a `/tcp` listener that remains online for at least one hour will receive a TLS certificate through the [`AutoTLS`](https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls) feature.
+This occurs automatically, with no need for manual setup.
+
+To bypass the 1-hour delay and enable AutoTLS immediately, users can explicitly opt-in by running the following commands:
+
+```console
+$ ipfs config --json AutoTLS.Enabled true
+$ ipfs config --json AutoTLS.RegistrationDelay 0
+```
+
+AutoTLS will remain disabled under the following conditions:
+
+- The node already has a manually configured `/ws` (WebSocket) listener
+- A private network is in use with a `swarm.key`
+- TCP or WebSocket transports are disabled, or there is no `/tcp` listener
+
+To troubleshoot, use `GOLOG_LOG_LEVEL="error,autotls=info`.
+
+For more details, check out the [`AutoTLS` configuration documentation](https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls) or dive deeper with [AutoTLS libp2p blog post](https://blog.libp2p.io/autotls/).
+
 #### RPC and CLI command changes
 
 - `ipfs config` is now validating json fields ([#10679](https://github.com/ipfs/kubo/pull/10679)).
diff --git a/docs/config.md b/docs/config.md
index ea4053f7a..7c1b7e40e 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -34,6 +34,7 @@ config file at runtime.
     - [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix)
     - [`AutoTLS.RegistrationEndpoint`](#autotlsregistrationendpoint)
     - [`AutoTLS.RegistrationToken`](#autotlsregistrationtoken)
+    - [`AutoTLS.RegistrationDelay`](#autotlsregistrationdelay)
     - [`AutoTLS.CAEndpoint`](#autotlscaendpoint)
   - [`Bootstrap`](#bootstrap)
   - [`Datastore`](#datastore)
@@ -463,12 +464,7 @@ Type: `duration` (when `0`/unset, the default value is used)
 
 ## `AutoTLS`
 
-> [!CAUTION]
-> This is an **EXPERIMENTAL** opt-in feature and should not be used in production yet.
-> Feel free to enable it and [report issues](https://github.com/ipfs/kubo/issues/new/choose) if you want to help with testing.
-> Track progress in [kubo#10560](https://github.com/ipfs/kubo/issues/10560).
-
-AutoTLS feature enables publicly reachable Kubo nodes (those dialable from the public
+The [AutoTLS](https://blog.libp2p.io/autotls/) feature enables publicly reachable Kubo nodes (those dialable from the public
 internet) to automatically obtain a wildcard TLS certificate for a DNS name
 unique to their PeerID at `*.[PeerID].libp2p.direct`. This enables direct
 libp2p connections and retrieval of IPFS content from browsers [Secure Context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)
@@ -480,11 +476,11 @@ broker enabling peer to obtain a wildcard TLS certificate tied to public key of
 
 By default, the certificates are requested from Let's Encrypt. Origin and rationale for this project can be found in [community.letsencrypt.org discussion](https://community.letsencrypt.org/t/feedback-on-raising-certificates-per-registered-domain-to-enable-peer-to-peer-networking/223003).
 
+<a href="https://ipshipyard.com/"><img align="right" src="https://github.com/user-attachments/assets/39ed3504-bb71-47f6-9bf8-cb9a1698f272" /></a>
+
 > [!NOTE]
 > Public good DNS and [p2p-forge] infrastructure at `libp2p.direct` is run by the team at [Interplanetary Shipyard](https://ipshipyard.com).
 >
-> <a href="https://ipshipyard.com/"><img src="https://github.com/user-attachments/assets/39ed3504-bb71-47f6-9bf8-cb9a1698f272" /></a>
-
 [p2p-forge]: https://github.com/ipshipyard/p2p-forge
 
 Default: `{}`
@@ -493,33 +489,22 @@ Type: `object`
 
 ### `AutoTLS.Enabled`
 
-> [!CAUTION]
-> This is an **EXPERIMENTAL** opt-in feature and should not be used in production yet.
-> Feel free to enable it and [report issues](https://github.com/ipfs/kubo/issues/new/choose) if you want to help with testing.
-> Track progress in [kubo#10560](https://github.com/ipfs/kubo/issues/10560).
+Enables the AutoTLS feature to provide DNS and TLS support for [libp2p Secure WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) over a `/tcp` port,
+to allow JS clients running in web browser [Secure Context](https://w3c.github.io/webappsec-secure-contexts/) to connect to Kubo directly.
 
-Enables AutoTLS feature to get DNS+TLS for [libp2p Secure WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) on `/tcp` port.
+When activated, together with [`AutoTLS.AutoWSS`](#autotlsautowss) (default) or manually including a `/tcp/{port}/tls/sni/*.libp2p.direct/ws` multiaddr in [`Addresses.Swarm`](#addressesswarm)
+(with SNI suffix matching [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix)), Kubo retrieves a trusted PKI TLS certificate for `*.{peerid}.libp2p.direct` and configures the `/ws` listener to use it.
 
-If `AutoTLS.AutoWSS` is `true`, or `/tcp/../tls/sni/*.libp2p.direct/ws` [multiaddr] is present in [`Addresses.Swarm`](#addressesswarm)
-with SNI segment ending with [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix),
-Kubo will obtain and set up a trusted PKI TLS certificate for `*.peerid.libp2p.direct`, making it dialable from web browser's [Secure Contexts](https://w3c.github.io/webappsec-secure-contexts/).
+**Note:**
 
-> [!TIP]
-> - Most users don't need custom `/ws` config in `Addresses.Swarm`. Try running this with `AutoTLS.AutoWSS=true`: it will reuse preexisting catch-all `/tcp` ports that were already forwarded/safelisted on your firewall.
-> - Debugging can be enabled by setting environment variable `GOLOG_LOG_LEVEL="error,autotls=debug,p2p-forge/client=debug"`. Less noisy `GOLOG_LOG_LEVEL="error,autotls=info` may be informative enough.
-> - Certificates are stored in `$IPFS_PATH/p2p-forge-certs`. Removing directory and restarting daemon will trigger certificate rotation.
-
-> [!IMPORTANT]
-> Caveats:
-> - Requires your Kubo node to be publicly dialable.
->   - If you want to test this with a node that is behind a NAT and uses manual TCP port forwarding or UPnP (`Swarm.DisableNatPortMap=false`), use `AutoTLS.AutoWSS=true`, or manually
->     add catch-all `/ip4/0.0.0.0/tcp/4001/tls/sni/*.libp2p.direct/ws` and `/ip6/::/tcp/4001/tls/sni/*.libp2p.direct/ws` to [`Addresses.Swarm`](#addressesswarm)
->     and **wait 5-15 minutes** for libp2p node to set up and learn about own public addresses via [AutoNAT](#autonat).
->   - If your node is fresh and just started, the [p2p-forge] client may produce and log ERRORs during this time, but once a publicly dialable addresses are set up, a subsequent retry should be successful.
-> - The TLS certificate is used only for [libp2p WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) connections.
->   - Right now, this is NOT used for hosting a [Gateway](#gateway) over HTTPS (that use case still requires manual TLS setup on reverse proxy, and your own domain).
+- This feature requires a publicly reachable node. If behind NAT, manual port forwarding or UPnP (`Swarm.DisableNatPortMap=false`) is required.
+- The first time AutoTLS is used, it may take 5-15 minutes + [`AutoTLS.RegistrationDelay`](#autotlsregistrationdelay) before `/ws` listener is added. Be patient.
+- Avoid manual configuration. [`AutoTLS.AutoWSS=true`](#autotlsautowss) should automatically add `/ws` listener to existing, firewall-forwarded `/tcp` ports.
+- To troubleshoot, use `GOLOG_LOG_LEVEL="error,autotls=debug` for detailed logs, or `GOLOG_LOG_LEVEL="error,autotls=info` for quieter output.
+- Certificates are stored in `$IPFS_PATH/p2p-forge-certs`; deleting this directory and restarting the daemon forces a certificate rotation.
+- For now, the TLS cert applies solely to `/ws` libp2p WebSocket connections, not HTTP [`Gateway`](#gateway), which still need separate reverse proxy TLS setup with a custom domain.
 
-Default: `false`
+Default: `true`
 
 Type: `flag`
 
@@ -527,7 +512,7 @@ Type: `flag`
 
 Optional. Controls if Kubo should add `/tls/sni/*.libp2p.direct/ws` listener to every pre-existing `/tcp` port IFF no explicit `/ws` is defined in [`Addresses.Swarm`](#addressesswarm) already.
 
-Default: `true` (active only if `AutoTLS.Enabled` is `true` as well)
+Default: `true` (if `AutoTLS.Enabled`)
 
 Type: `flag`
 
@@ -535,8 +520,7 @@ Type: `flag`
 
 Optional. Controls if final AutoTLS listeners are announced under shorter `/dnsX/A.B.C.D.peerid.libp2p.direct/tcp/4001/tls/ws` addresses instead of fully resolved `/ip4/A.B.C.D/tcp/4001/tls/sni/A-B-C-D.peerid.libp2p.direct/tls/ws`.
 
-> [!TIP]
-> The main use for AutoTLS is allowing connectivity from Secure Context in a web browser, and DNS lookup needs to happen there anyway, making `/dnsX` a more compact, more interoperable option without obvious downside.
+The main use for AutoTLS is allowing connectivity from Secure Context in a web browser, and DNS lookup needs to happen there anyway, making `/dnsX` a more compact, more interoperable option without obvious downside.
 
 Default: `true`
 
@@ -574,6 +558,17 @@ Default: `""`
 
 Type: `optionalString`
 
+### `AutoTLS.RegistrationDelay`
+
+An additional delay applied before sending a request to the `RegistrationEndpoint`.
+
+The default delay is bypassed if the user explicitly set `AutoTLS.Enabled=true` in the JSON configuration file.
+This ensures that ephemeral nodes using the default configuration do not spam the`AutoTLS.CAEndpoint` with unnecessary ACME requests.
+
+Default: `1h` (or `0` if explicit `AutoTLS.Enabled=true`)
+
+Type: `optionalDuration`
+
 ### `AutoTLS.CAEndpoint`
 
 Optional override of CA ACME API used by [p2p-forge] system.
diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod
index 812248eef..9c6744fea 100644
--- a/docs/examples/kubo-as-a-library/go.mod
+++ b/docs/examples/kubo-as-a-library/go.mod
@@ -111,7 +111,7 @@ require (
 	github.com/ipld/go-car/v2 v2.14.2 // indirect
 	github.com/ipld/go-codec-dagpb v1.6.0 // indirect
 	github.com/ipld/go-ipld-prime v0.21.0 // indirect
-	github.com/ipshipyard/p2p-forge v0.3.0 // indirect
+	github.com/ipshipyard/p2p-forge v0.4.0 // indirect
 	github.com/jackpal/go-nat-pmp v1.0.2 // indirect
 	github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
 	github.com/jbenet/goprocess v0.1.4 // indirect
diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum
index a379310ac..c3a4acd27 100644
--- a/docs/examples/kubo-as-a-library/go.sum
+++ b/docs/examples/kubo-as-a-library/go.sum
@@ -405,8 +405,8 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH
 github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
 github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
 github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw=
-github.com/ipshipyard/p2p-forge v0.3.0 h1:mdeFqiq8ljX149OCQvveV0vOlKeIt4PWkJjXVfux/GE=
-github.com/ipshipyard/p2p-forge v0.3.0/go.mod h1:L0TJMzniMEDjX8G+RB201U2woHvASwbsujNVDNVivDo=
+github.com/ipshipyard/p2p-forge v0.4.0 h1:A40PW027nCZn0d/pw5ufMoxnAo9wZwrG+iuz18A8P5I=
+github.com/ipshipyard/p2p-forge v0.4.0/go.mod h1:hVGPP24xrRezP2+z6q8udEW36w89M+jWuWBz9meLggY=
 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
 github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
 github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
diff --git a/go.mod b/go.mod
index ffea67eee..948527983 100644
--- a/go.mod
+++ b/go.mod
@@ -49,7 +49,7 @@ require (
 	github.com/ipld/go-car/v2 v2.14.2
 	github.com/ipld/go-codec-dagpb v1.6.0
 	github.com/ipld/go-ipld-prime v0.21.0
-	github.com/ipshipyard/p2p-forge v0.3.0
+	github.com/ipshipyard/p2p-forge v0.4.0
 	github.com/jbenet/go-temp-err-catcher v0.1.0
 	github.com/jbenet/goprocess v0.1.4
 	github.com/julienschmidt/httprouter v1.3.0
diff --git a/go.sum b/go.sum
index bf2b54ac8..74604807e 100644
--- a/go.sum
+++ b/go.sum
@@ -473,8 +473,8 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH
 github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
 github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
 github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw=
-github.com/ipshipyard/p2p-forge v0.3.0 h1:mdeFqiq8ljX149OCQvveV0vOlKeIt4PWkJjXVfux/GE=
-github.com/ipshipyard/p2p-forge v0.3.0/go.mod h1:L0TJMzniMEDjX8G+RB201U2woHvASwbsujNVDNVivDo=
+github.com/ipshipyard/p2p-forge v0.4.0 h1:A40PW027nCZn0d/pw5ufMoxnAo9wZwrG+iuz18A8P5I=
+github.com/ipshipyard/p2p-forge v0.4.0/go.mod h1:hVGPP24xrRezP2+z6q8udEW36w89M+jWuWBz9meLggY=
 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
 github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
 github.com/jbenet/go-cienv v0.1.0 h1:Vc/s0QbQtoxX8MwwSLWWh+xNNZvM3Lw7NsTcHrvvhMc=



Test patch:
diff --git a/core/coreapi/test/api_test.go b/core/coreapi/test/api_test.go
index d647a32c8..dfd8cf685 100644
--- a/core/coreapi/test/api_test.go
+++ b/core/coreapi/test/api_test.go
@@ -69,6 +69,7 @@ func (NodeProvider) MakeAPISwarm(t *testing.T, ctx context.Context, fullIdentity
 		c.Addresses.Swarm = []string{fmt.Sprintf("/ip4/18.0.%d.1/tcp/4001", i)}
 		c.Identity = ident
 		c.Experimental.FilestoreEnabled = true
+		c.AutoTLS.Enabled = config.False // disable so no /ws listener is added
 
 		ds := syncds.MutexWrap(datastore.NewMapDatastore())
 		r := &repo.Mock{
diff --git a/test/cli/transports_test.go b/test/cli/transports_test.go
index cbef5c57d..ec27e00ba 100644
--- a/test/cli/transports_test.go
+++ b/test/cli/transports_test.go
@@ -94,6 +94,7 @@ func TestTransports(t *testing.T) {
 				cfg.Swarm.Transports.Network.QUIC = config.True
 				cfg.Swarm.Transports.Network.WebTransport = config.False
 				cfg.Swarm.Transports.Network.WebRTCDirect = config.False
+				cfg.Swarm.Transports.Network.Websocket = config.False
 			})
 		})
 		disableRouting(nodes)
@@ -111,6 +112,7 @@ func TestTransports(t *testing.T) {
 				cfg.Swarm.Transports.Network.QUIC = config.True
 				cfg.Swarm.Transports.Network.WebTransport = config.True
 				cfg.Swarm.Transports.Network.WebRTCDirect = config.False
+				cfg.Swarm.Transports.Network.Websocket = config.False
 			})
 		})
 		disableRouting(nodes)
@@ -150,6 +152,7 @@ func TestTransports(t *testing.T) {
 				cfg.Swarm.Transports.Network.QUIC = config.False
 				cfg.Swarm.Transports.Network.WebTransport = config.False
 				cfg.Swarm.Transports.Network.WebRTCDirect = config.True
+				cfg.Swarm.Transports.Network.Websocket = config.False
 			})
 		})
 		disableRouting(nodes)
diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod
index f07d6e578..aec0d66a2 100644
--- a/test/dependencies/go.mod
+++ b/test/dependencies/go.mod
@@ -130,7 +130,7 @@ require (
 	github.com/ipfs/kubo v0.31.0 // indirect
 	github.com/ipld/go-codec-dagpb v1.6.0 // indirect
 	github.com/ipld/go-ipld-prime v0.21.0 // indirect
-	github.com/ipshipyard/p2p-forge v0.3.0 // indirect
+	github.com/ipshipyard/p2p-forge v0.4.0 // indirect
 	github.com/jackpal/go-nat-pmp v1.0.2 // indirect
 	github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
 	github.com/jbenet/goprocess v0.1.4 // indirect
diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum
index 64f77b63b..b88673550 100644
--- a/test/dependencies/go.sum
+++ b/test/dependencies/go.sum
@@ -365,8 +365,8 @@ github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6
 github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
 github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
 github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
-github.com/ipshipyard/p2p-forge v0.3.0 h1:mdeFqiq8ljX149OCQvveV0vOlKeIt4PWkJjXVfux/GE=
-github.com/ipshipyard/p2p-forge v0.3.0/go.mod h1:L0TJMzniMEDjX8G+RB201U2woHvASwbsujNVDNVivDo=
+github.com/ipshipyard/p2p-forge v0.4.0 h1:A40PW027nCZn0d/pw5ufMoxnAo9wZwrG+iuz18A8P5I=
+github.com/ipshipyard/p2p-forge v0.4.0/go.mod h1:hVGPP24xrRezP2+z6q8udEW36w89M+jWuWBz9meLggY=
 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
 github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
 github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=