diff --git a/client/client.yml b/client/client.yml
index 9f62990a321c1816acae5b13507444ee693bfcc7..56733a14b3eb5023efa380c013e2d1f486a363e1 100644
--- a/client/client.yml
+++ b/client/client.yml
@@ -16,6 +16,10 @@
 #         command: 'echo "$message"'
 #         if:
 #             priority: high,urgent
+#       - topic: secret
+#         command: 'notify-send "$m"'
+#         user: phill
+#         password: mypass
 #
 # Variables:
 #     Variable        Aliases               Description
diff --git a/client/config.go b/client/config.go
index c44fac6c1baae80e05c9941df7f0ed5c3516b054..0866cd1bfc1264749b58692a981e736209d08574 100644
--- a/client/config.go
+++ b/client/config.go
@@ -14,9 +14,11 @@ const (
 type Config struct {
 	DefaultHost string `yaml:"default-host"`
 	Subscribe   []struct {
-		Topic   string            `yaml:"topic"`
-		Command string            `yaml:"command"`
-		If      map[string]string `yaml:"if"`
+		Topic    string            `yaml:"topic"`
+		User     string            `yaml:"user"`
+		Password string            `yaml:"password"`
+		Command  string            `yaml:"command"`
+		If       map[string]string `yaml:"if"`
 	} `yaml:"subscribe"`
 }
 
diff --git a/client/config_test.go b/client/config_test.go
index 8d322111794f0db1262c230a25cd41d1f2910742..d601cdb4ffe1eb2ef4337736e1299deb5ce56e31 100644
--- a/client/config_test.go
+++ b/client/config_test.go
@@ -13,7 +13,9 @@ func TestConfig_Load(t *testing.T) {
 	require.Nil(t, os.WriteFile(filename, []byte(`
 default-host: http://localhost
 subscribe:
-  - topic: no-command
+  - topic: no-command-with-auth
+    user: phil
+    password: mypass
   - topic: echo-this
     command: 'echo "Message received: $message"'
   - topic: alerts
@@ -26,8 +28,10 @@ subscribe:
 	require.Nil(t, err)
 	require.Equal(t, "http://localhost", conf.DefaultHost)
 	require.Equal(t, 3, len(conf.Subscribe))
-	require.Equal(t, "no-command", conf.Subscribe[0].Topic)
+	require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
 	require.Equal(t, "", conf.Subscribe[0].Command)
+	require.Equal(t, "phil", conf.Subscribe[0].User)
+	require.Equal(t, "mypass", conf.Subscribe[0].Password)
 	require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
 	require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
 	require.Equal(t, "alerts", conf.Subscribe[2].Topic)
diff --git a/cmd/subscribe.go b/cmd/subscribe.go
index b5a56933410330833a15a1c1412ac0473002fde6..9000a163497b58faebd746b1e68ba552f490be4b 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -23,6 +23,7 @@ var cmdSubscribe = &cli.Command{
 	Flags: []cli.Flag{
 		&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
 		&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
+		&cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"},
 		&cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"},
 		&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
 		&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
@@ -40,6 +41,7 @@ ntfy subscribe TOPIC
     ntfy subscribe mytopic            # Prints JSON for incoming messages for ntfy.sh/mytopic
     ntfy sub home.lan/backups         # Subscribe to topic on different server
     ntfy sub --poll home.lan/backups  # Just query for latest messages and exit
+    ntfy sub -u phil:mypass secret    # Subscribe with username/password
   
 ntfy subscribe TOPIC COMMAND
   This executes COMMAND for every incoming messages. The message fields are passed to the
@@ -81,6 +83,7 @@ func execSubscribe(c *cli.Context) error {
 	}
 	cl := client.New(conf)
 	since := c.String("since")
+	user := c.String("user")
 	poll := c.Bool("poll")
 	scheduled := c.Bool("scheduled")
 	fromConfig := c.Bool("from-config")
@@ -93,6 +96,23 @@ func execSubscribe(c *cli.Context) error {
 	if since != "" {
 		options = append(options, client.WithSince(since))
 	}
+	if user != "" {
+		var pass string
+		parts := strings.SplitN(user, ":", 2)
+		if len(parts) == 2 {
+			user = parts[0]
+			pass = parts[1]
+		} else {
+			fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
+			p, err := util.ReadPassword(c.App.Reader)
+			if err != nil {
+				return err
+			}
+			pass = string(p)
+			fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
+		}
+		options = append(options, client.WithBasicAuth(user, pass))
+	}
 	if poll {
 		options = append(options, client.WithPoll())
 	}
@@ -142,6 +162,9 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
 		for filter, value := range s.If {
 			topicOptions = append(topicOptions, client.WithFilter(filter, value))
 		}
+		if s.User != "" && s.Password != "" {
+			topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
+		}
 		subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
 		commands[subscriptionID] = s.Command
 	}
diff --git a/docs/examples.md b/docs/examples.md
index f7c85d70173e6d3f18dc39da5f704a53601c558e..67aa73ffbeb2ddb5dcd14378ebb223a2d1f09ca7 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -75,3 +75,21 @@ One of my co-workers uses the following Ansible task to let him know when things
     method: POST
     body: "{{ inventory_hostname }} reseeding complete"
 ```
+
+## Watchtower notifications (shoutrrr)
+You can use `shoutrrr` generic webhook support to send watchtower notifications to your ntfy topic.
+
+Example docker-compose.yml:
+```yml
+services:
+  watchtower:
+    image: containrrr/watchtower
+    environment:
+      - WATCHTOWER_NOTIFICATIONS=shoutrrr
+      - WATCHTOWER_NOTIFICATION_URL=generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
+```
+
+Or, if you only want to send notifications using shoutrrr:
+```
+shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
+```
diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md
index 2d3f83b4bb62915867fb5d7388c50b2aa212752e..52e005c0badb52ab6e523ced2c717be1610c3387 100644
--- a/docs/subscribe/cli.md
+++ b/docs/subscribe/cli.md
@@ -196,3 +196,27 @@ EOF
 sudo systemctl daemon-reload
 sudo systemctl restart ntfy-client
 ```
+
+
+### Authentication
+Depending on whether the server is configured to support [access control](../config.md#access-control), some topics
+may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
+To publish/subscribe to protected topics, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
+with a valid username/password. For your self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing
+your password. 
+
+You can either add your username and password to the configuration file:
+=== "~/.config/ntfy/client.yml"
+	```yaml
+	 - topic: secret
+	   command: 'notify-send "$m"'
+	   user: phill
+	   password: mypass
+	```
+
+Or with the `ntfy subscibe` command:
+```
+ntfy subscribe \
+  -u phil:mypass \
+  ntfy.example.com/mysecrets
+```