diff --git a/ironic/conf/vnc.py b/ironic/conf/vnc.py
index 47954ba08a..b8891b602f 100644
--- a/ironic/conf/vnc.py
+++ b/ironic/conf/vnc.py
@@ -71,6 +71,12 @@ opts = [
         default=['none'],
         help='The allowed authentication schemes to use with proxied '
              'VNC connections'),
+    cfg.BoolOpt(
+        'read_only',
+        default=False,
+        help='When True, keyboard and mouse events will not be passed '
+             'to the console.'
+    ),
     cfg.IntOpt(
         'token_timeout',
         default=600,
diff --git a/ironic/console/container/ironic-console.container.template b/ironic/console/container/ironic-console.container.template
index 32dc0ddcbe..2000582deb 100644
--- a/ironic/console/container/ironic-console.container.template
+++ b/ironic/console/container/ironic-console.container.template
@@ -6,6 +6,7 @@ Image={{ image }}
 PublishPort={{ port }}
 Environment=APP={{ app }}
 Environment=APP_INFO='{{ app_info }}'
+Environment=READ_ONLY={{ read_only }}
 
 [Install]
 WantedBy=default.target
\ No newline at end of file
diff --git a/ironic/console/container/systemd.py b/ironic/console/container/systemd.py
index ed78c938a5..178145fb4d 100644
--- a/ironic/console/container/systemd.py
+++ b/ironic/console/container/systemd.py
@@ -192,6 +192,7 @@ class SystemdConsoleContainer(base.BaseConsoleContainer):
                 'port': CONF.vnc.systemd_container_publish_port,
                 'app': app_name,
                 'app_info': json.dumps(app_info),
+                'read_only': CONF.vnc.read_only,
             }
 
             LOG.debug('Writing %s', container_file)
diff --git a/ironic/tests/unit/console/container/test_console_container.py b/ironic/tests/unit/console/container/test_console_container.py
index 67f3995276..64c85870ef 100644
--- a/ironic/tests/unit/console/container/test_console_container.py
+++ b/ironic/tests/unit/console/container/test_console_container.py
@@ -183,6 +183,10 @@ class TestSystemdConsoleContainer(base.TestCase):
             'console_image',
             'localhost/ironic-vnc-container',
             group='vnc')
+        CONF.set_override(
+            'read_only',
+            True,
+            group='vnc')
 
         uuid = '1234'
         container_path = self.provider._container_path(uuid)
@@ -200,6 +204,7 @@ Image=localhost/ironic-vnc-container
 PublishPort=192.0.2.2::5900
 Environment=APP=fake
 Environment=APP_INFO='{}'
+Environment=READ_ONLY=True
 
 [Install]
 WantedBy=default.target""", f.read())
diff --git a/releasenotes/notes/vnc_read_only-e0f18c5d0d356515.yaml b/releasenotes/notes/vnc_read_only-e0f18c5d0d356515.yaml
new file mode 100644
index 0000000000..57473b2a0d
--- /dev/null
+++ b/releasenotes/notes/vnc_read_only-e0f18c5d0d356515.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    When ``ironic.conf`` ``[vnc]read_only=True`` is set, keyboard and mouse
+    events will not be passed to the console.
\ No newline at end of file
diff --git a/tools/vnc-container/bin/start-browser-x11vnc.sh b/tools/vnc-container/bin/start-browser-x11vnc.sh
index 822695a5b7..7e314f9da1 100755
--- a/tools/vnc-container/bin/start-browser-x11vnc.sh
+++ b/tools/vnc-container/bin/start-browser-x11vnc.sh
@@ -2,4 +2,10 @@
 
 set -eux
 
-x11vnc -nevershared -forever -afteraccept 'start-selenium-browser.py &' -gone 'killall -s SIGTERM python3'
\ No newline at end of file
+if [ "$READ_ONLY" = "True" ]; then
+    viewonly="-viewonly"
+else
+    viewonly=""
+fi
+
+x11vnc $viewonly -nevershared -forever -afteraccept 'start-selenium-browser.py &' -gone 'killall -s SIGTERM python3'
\ No newline at end of file