Repository: scionproto/scion
Pull request #4503 (2024-04-08 16:03:10)

doc: extend linters, manage deps via bazel
- use markdownlint-cli2 instead of ruby markdownlint

  Use Node.js-based github.com/DavidAnson/markdownlint-cli2, replacing the ruby-based github.com/markdownlint/markdownlint. This tool has a cleaner configuration file and that is well supported in vs code and other editors.

  Adapted from https://github.com/scionproto/scion/pull/4328.

- add sphinx-lint, and manage the doc requirements via bazel/rules_python.

  Fix a large number of legitimage linter warnings; most cases were accidentally applying the "default role" (single backticks instead of double-backticks), or the occasional missing underscore after external links.

- add bazel run targets to run sphinx-build and sphinx-autobuild via python deps managed by bazel. This makes it easy to locally build the documentation for devs that already have bazel set up (while hopefully keeping things still relatively straight-forward).

matzf: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/4503)
<!-- Reviewable:end -->


Patch:
diff --git a/.bazelignore b/.bazelignore
index 26c872b96..c99fb3545 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -1,5 +1,6 @@
 bin
 doc/_build
+doc/venv
 docker/_build
 private/mgmtapi/tools/node_modules
 tools/lint/logctxcheck/testdata/src
diff --git a/.bazelrc b/.bazelrc
index 661583c3e..d3110f0b5 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -35,7 +35,7 @@ test:unit_all --config=unit //...
 test:integration --test_tag_filters=integration,-lint
 test:integration_all --config=integration //...
 
-test:lint --test_tag_filters=lint --test_summary=terse --noshow_progress --experimental_convenience_symlinks=ignore //...
+test:lint --test_tag_filters=lint --test_summary=terse --noshow_progress --experimental_convenience_symlinks=ignore
 
 # run quietly, only display errors
 common:quiet --ui_event_filters=-warning,-info,-debug,-stdout,-stderr --noshow_progress
diff --git a/.github/ISSUE_TEMPLATE/01-proposal.md b/.github/ISSUE_TEMPLATE/01-proposal.md
index 0a538d914..05f316350 100644
--- a/.github/ISSUE_TEMPLATE/01-proposal.md
+++ b/.github/ISSUE_TEMPLATE/01-proposal.md
@@ -8,4 +8,3 @@ labels: i/proposal
 Our proposal process is documented here:
 https://docs.scion.org/en/latest/dev/contribute.html#change-proposal-process
 -->
-
diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc
new file mode 100644
index 000000000..7be2f0b20
--- /dev/null
+++ b/.markdownlint-cli2.jsonc
@@ -0,0 +1,27 @@
+{
+    "globs": ["**/*.md"],
+    "ignores": [
+        "**/venv/**",
+        "**/_build/**",
+        "**/node_modules/**",
+        "licenses/data/**",
+        "tools/coremark/LICENSE.md"
+    ],
+    "config": {
+        "default": true,
+        "MD007": {
+            "indent": 4
+        },
+        "MD013": {
+            "line_length": 100,
+            "code_blocks": false,
+            "tables": false
+        },
+        "MD024": {
+            "siblings_only": true
+        }
+    },
+    "outputFormatters": [
+        [ "markdownlint-cli2-formatter-pretty", { "appendLink": true } ]
+    ]
+}
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index cfcdbd001..36699c0dd 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -7,7 +7,7 @@ version: 2
 build:
   os: ubuntu-22.04
   tools:
-    python: "3.8"
+    python: "3.10"
 
 sphinx:
    configuration: doc/conf.py
diff --git a/Makefile b/Makefile
index 638e6ad1e..f34413c51 100644
--- a/Makefile
+++ b/Makefile
@@ -96,7 +96,7 @@ antlr:
 write_all_source_files:
 	bazel run //:write_all_source_files
 
-.PHONY: lint lint-bazel lint-bazel-buildifier lint-doc lint-doc-mdlint lint-go lint-go-bazel lint-go-gazelle lint-go-golangci lint-go-semgrep lint-openapi lint-openapi-spectral lint-protobuf lint-protobuf-buf
+.PHONY: lint lint-bazel lint-bazel-buildifier lint-doc lint-doc-mdlint lint-doc-sphinx lint-go lint-go-bazel lint-go-gazelle lint-go-golangci lint-go-semgrep lint-openapi lint-openapi-spectral lint-protobuf lint-protobuf-buf
 
 # Enable --keep-going if all goals specified on the command line match the pattern "lint%"
 ifeq ($(filter-out lint%, $(MAKECMDGOALS)), )
@@ -113,7 +113,7 @@ lint-go-gazelle:
 
 lint-go-bazel:
 	$(info ==> $@)
-	@tools/quiet bazel test --config lint
+	@tools/quiet bazel test --config lint //...
 
 GO_BUILD_TAGS_ARG=$(shell bazel build --ui_event_filters=-stdout,-stderr --announce_rc --noshow_progress :dummy_setting 2>&1 | grep "'build' options" | sed -n "s/^.*--define gotags=\(\S*\).*/--build-tags \1/p" )
 
@@ -149,9 +149,13 @@ lint-openapi-spectral:
 	$(info ==> $@)
 	@tools/quiet bazel run --config=quiet //:spectral -- lint --ruleset ${PWD}/spec/.spectral.yml ${PWD}/spec/*.gen.yml
 
-lint-doc: lint-doc-mdlint
+lint-doc: lint-doc-mdlint lint-doc-sphinx
 
 lint-doc-mdlint:
 	$(info ==> $@)
-	@FILES=$$(find -type f -iname '*.md' -not -path "./private/mgmtapi/tools/node_modules/*" -not -path "./.github/**/*" | grep -vf tools/md/skipped); \
-		docker run --rm -v ${PWD}:/data -v ${PWD}/tools/md/mdlintstyle.rb:/style.rb $$(docker build -q tools/md) $${FILES} -s /style.rb
+	@if [ -t 1 ]; then tty=true; else tty=false; fi; \
+		tools/quiet docker run --tty=$$tty --rm -v ${PWD}:/workdir davidanson/markdownlint-cli2:v0.12.1
+
+lint-doc-sphinx:
+	$(info ==> $@)
+	@tools/quiet bazel test --config=lint //doc:sphinx_lint_test
diff --git a/WORKSPACE b/WORKSPACE
index e12c7da5d..163fa20db 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -111,6 +111,14 @@ load("@com_github_scionproto_scion_python_deps//:requirements.bzl", install_pyth
 
 install_python_deps()
 
+load("//doc:deps.bzl", "python_doc_deps")
+
+python_doc_deps(python_interpreter)
+
+load("@com_github_scionproto_scion_python_doc_deps//:requirements.bzl", install_python_doc_deps = "install_deps")
+
+install_python_doc_deps()
+
 http_archive(
     name = "rules_pkg",
     sha256 = "8f9ee2dc10c1ae514ee599a8b42ed99fa262b757058f65ad3c384289ff70c4b8",
diff --git a/doc/.markdownlint-cli2.jsonc b/doc/.markdownlint-cli2.jsonc
new file mode 100644
index 000000000..d3abaacd3
--- /dev/null
+++ b/doc/.markdownlint-cli2.jsonc
@@ -0,0 +1,11 @@
+{
+    "extends": "../.markdownlint-cli2.jsonc",
+    "config": {
+        "MD033": {
+            "allowed_elements": [
+                "a",
+                "sub"
+            ]
+        }
+    }
+}
diff --git a/doc/BUILD.bazel b/doc/BUILD.bazel
index e69de29bb..a8da7fc6a 100644
--- a/doc/BUILD.bazel
+++ b/doc/BUILD.bazel
@@ -0,0 +1,58 @@
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
+load("@com_github_scionproto_scion_python_doc_deps//:requirements.bzl", "all_requirements", "entry_point", "requirement")
+load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
+load(":lint.bzl", "sphinx_lint_test")
+
+# This BUILD file helps to manage requirements for the sphinx build tools.
+# It assists in building the documentation locally, for developers that already
+# have the bazel setup, as an alternative to manually installing the dependencies from ./requirements.txt.
+#
+# Build the documentation with the accompanying ./Makefile.
+#
+# Note that for builds on the readthedocs infrastructure, this Makefile or bazel is not used!
+# See ../.readthedocs.yaml
+
+compile_pip_requirements(
+    name = "requirements",
+    requirements_in = ":requirements.in",
+    requirements_txt = ":requirements.txt",
+)
+
+# sphinx-build:
+# Needs more than "entry_point" because for the extensions, we need to include more dependencies than the default requirements of the package.
+# Run with all packages from requirements.txt so we don't have to explicitly repeat the required modules.
+py_console_script_binary(
+    name = "sphinx-build",
+    pkg = requirement("sphinx"),  # note: "requirement" is just a shorthand for the :pkg label referencing the py_library
+    script = "sphinx-build",
+    deps = [r for r in all_requirements if r != requirement("sphinx")],  # macro adds "pkg" to deps, filter it out to avoid duplicate value in attribute
+)
+
+# sphinx-autobuild: just like above
+py_console_script_binary(
+    name = "sphinx-autobuild",
+    pkg = requirement("sphinx-autobuild"),
+    script = "sphinx-autobuild",
+    deps = [r for r in all_requirements if r != requirement("sphinx-autobuild")],
+)
+
+# sphinx-lint
+alias(
+    name = "sphinx-lint",
+    actual = entry_point("sphinx-lint"),
+)
+
+sphinx_lint_test(
+    name = "sphinx_lint_test",
+    srcs = glob([
+        "**/*.rst",
+        "**/*.md",
+    ]),
+    args = [
+        "--enable",
+        "all",
+        "--disable",
+        "line-too-long",
+    ],
+    tags = ["lint"],
+)
diff --git a/doc/Makefile b/doc/Makefile
index a7c09b9df..a7278e93c 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -1,32 +1,37 @@
-# These variables can be overridden from the command line from the environment.
+# This Makefile helps to build the sphinx documentation.
+# This runs tools using python dependencies managed via bazel/rules_python for the convenience of contributors
+# that have already set up bazel.
+# Alternatively, with dependencies from ./requirements.txt installed with pip, e.g. in a venv,
+# this Makefile can be used directly invoking the build tool (e.g "make SPHINX_BUILD=sphinx-build")
+#
+# Note that for builds on the readthedocs infrastructure, this Makefile or bazel is not used!
+# See ../.readthedocs.yaml
+
+# Variables can be overridden from the command line from the environment.
+SPHINX_BUILD	 ?= bazel run //doc:sphinx-build --
+SPHINX_AUTOBUILD ?= bazel run //doc:sphinx-autobuild --
 SPHINXOPTS    ?= -W --keep-going # treat warnings as errors, but process all files when an error occurs
-SOURCEDIR     = .
-BUILDDIR      = _build
+SOURCEDIR     = $(realpath .)
+BUILDDIR      = $(realpath _build)
 
 # Build docs with Sphinx using the "make mode" option.
 # Explicitly list the main build targets so they auto-complete in shells.
 # The first listed name is the default goal (i.e. `make` is `make html`).
 .PHONY: html latex latexpdf linkcheck help
 html latex latexpdf linkcheck help:
-	sphinx-build -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
+	$(SPHINX_BUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
 
 # Catch-all pattern to match any unknown target, too.
 .PHONY: %
 %:
-	sphinx-build -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
+	$(SPHINX_BUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
 
 # Autobuild; automatically build HTML on source changes and serve on localhost
 # Note: most options are forwarded as-is to sphinx, but --keep-going is not understood and we explicitly drop it.
 .PHONY: autobuild
 autobuild:
-	sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(filter-out --keep-going,$(SPHINXOPTS))
+	$(SPHINX_AUTOBUILD) "$(SOURCEDIR)" "$(BUILDDIR)" $(filter-out --keep-going,$(SPHINXOPTS))
 
 .PHONY: clean
 clean:
 	rm -rf _build
-
-# Generate commandline docs by building the tools and running their `gendocus` subcommand
-.PHONY: command-gendocs
-command-gendocs:
-	bazel run //doc/command:copy_scion
-	bazel run //doc/command:copy_scion-pki
diff --git a/doc/control-plane.rst b/doc/control-plane.rst
index 62731987b..abb20d427 100644
--- a/doc/control-plane.rst
+++ b/doc/control-plane.rst
@@ -271,7 +271,7 @@ path-lookup on behalf of applications. This SCION daemon also caches path-segmen
 Path-Segment Combination
 ========================
 
-The last step of the path-resolution process is to combine the available up-, core- and down-
+The last step of the path-resolution process is to combine the available up, core and down
 path segments to end-to-end forwarding paths.
 This path-segment combination process is done by each endpoint separately.
 Typically, end hosts run the :doc:`SCION daemon <manuals/daemon>` which centralizes the
diff --git a/doc/cryptography/certificates.rst b/doc/cryptography/certificates.rst
index 76e7973ed..f0df02114 100644
--- a/doc/cryptography/certificates.rst
+++ b/doc/cryptography/certificates.rst
@@ -105,7 +105,7 @@ Version
 -------
 
 The ``version`` field is always ``v3`` in SCION; this is required because
-`extensions` are mandatory.
+``extensions`` are mandatory.
 
 **Deprecation warning**: note that the X.509 ``version`` field has different
 semantics compared to the old SCION JSON format for certificates (where version
@@ -247,14 +247,14 @@ The string representation MUST follow the canonical formatting defined in `ISD
 and AS numbering
 <https://github.com/scionproto/scion/wiki/ISD-and-AS-numbering>`_.
 
-The canonical string representation of the *ISD-AS number* uses a `-`-separator
+The canonical string representation of the *ISD-AS number* uses a ``-``-separator
 between the ISD and AS numbers.
 
 The ISD numbers are formatted as decimal.
 
 The canonical string formatting of AS numbers in the BGP AS range (
 :math:`0 - 2^{32}-1`) is the decimal form.
-For larger AS numbers, it uses a 16-bit `:`-separated lower-case hex encoding
+For larger AS numbers, it uses a 16-bit ``:``-separated lower-case hex encoding
 with leading 0's omitted: 1:0:0 to ffff:ffff:ffff.
 
 For example, AS ``ff00:0:110`` in ISD ``1`` is formatted as ``1-ff00:0:110``.
diff --git a/doc/cryptography/drkey.rst b/doc/cryptography/drkey.rst
index 23b10db46..95ba17e26 100644
--- a/doc/cryptography/drkey.rst
+++ b/doc/cryptography/drkey.rst
@@ -279,12 +279,12 @@ the CP-PKI infrastructure.
 
 The Level 1 key request message contains the ``validTime`` that specifies for what time the requested
 key must be valid (it implicitly specifies the epoch for which the key will be valid)
-and the ``protocol_id``. 
+and the ``protocol_id``.
 The Level 1 key response includes the symmetric key along with the epoch
 for which this key will be valid.
 
 The ``protocol_id`` is either set to ``GENERIC = 0`` to request Lvl1 keys that will be derived according to
-the `generic-protocol` hierarchy or to the protocol number for the `protocol-specific` derivation.
+the ``generic-protocol`` hierarchy or to the protocol number for the ``protocol-specific`` derivation.
 
 Level 0/2/3 key establishment
 -----------------------------
@@ -307,8 +307,8 @@ information (depending on the key type).
 The server responds with the symmetric key and the epoch.
 
 The ``protocol_id`` in Lvl2/3 requests is always set to the final protocol identifier.
-The key service will choose between the `protocol-specific` derivation, if it exists, or
-the `generic-protocol` derivation, otherwise.
+The key service will choose between the ``protocol-specific`` derivation, if it exists, or
+the ``generic-protocol`` derivation, otherwise.
 
 Spreading Level 1 key requests
 ==============================
diff --git a/doc/cryptography/trc-signing-ceremony-phases-sensitive.rst b/doc/cryptography/trc-signing-ceremony-phases-sensitive.rst
index 9b7de7dfc..e6232a24e 100644
--- a/doc/cryptography/trc-signing-ceremony-phases-sensitive.rst
+++ b/doc/cryptography/trc-signing-ceremony-phases-sensitive.rst
@@ -143,7 +143,7 @@ below.
 
 Seventh, ask the *voting representatives* which voters from the predecessor TRC
 should take part in the voting process. The value will be used to fill the
-``{{.Votes}}`` variable below. Votes contains a sequence of indices of the voting 
+``{{.Votes}}`` variable below. Votes contains a sequence of indices of the voting
 certificates in the predecessor TRC. To find the indices, you can use the ``scion-pki
 trcs human`` command.
 
diff --git a/doc/cryptography/trc.rst b/doc/cryptography/trc.rst
index a3353977c..02c2a75ad 100644
--- a/doc/cryptography/trc.rst
+++ b/doc/cryptography/trc.rst
@@ -111,7 +111,7 @@ version is always ``v1``.
 
 **Deprecation warning**: note that the ``version`` field has different semantics
 compared to the old SCION JSON format for TRCs (where version was an
-incrementing counter). The JSON style `version` is moved to the `serial number`
+incrementing counter). The JSON style ``version`` is moved to the ``serial number``
 field in the :ref:`trc-id-field` sequence bellow.
 
 .. _trc-id-field:
@@ -347,7 +347,7 @@ We build on top of the rules from [RFC5652]_ and add the following restrictions:
 - The ``certificates`` field in ``SignedData`` is left empty. The certificate
   pool used to verify the TRC updates is based on the previous TRC.
 - The ``eContentType`` is set to ``id-data``. The contents of the ``eContent``
-  is the DER encoded `TRCPayload`, as specified above. This has the benefit that
+  is the DER encoded ``TRCPayload``, as specified above. This has the benefit that
   the format is backwards compatible with PKCS #7, as described in [RFC5652]_,
   Section 5.2.1.
 - Because we do not include certificates in ``SignedData`` and choose
diff --git a/doc/deps.bzl b/doc/deps.bzl
new file mode 100644
index 000000000..80c3e420b
--- /dev/null
+++ b/doc/deps.bzl
@@ -0,0 +1,10 @@
+""" Python dependency declaration """
+
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+def python_doc_deps(interpreter):
+    pip_parse(
+        name = "com_github_scionproto_scion_python_doc_deps",
+        python_interpreter_target = interpreter,
+        requirements = "@com_github_scionproto_scion//doc:requirements.txt",
+    )
diff --git a/doc/dev/design/BeaconService.md b/doc/dev/design/BeaconService.md
index ac9e51444..1d21ecd71 100644
--- a/doc/dev/design/BeaconService.md
+++ b/doc/dev/design/BeaconService.md
@@ -4,7 +4,8 @@
 * Status: **outdated**
 
 ---
-⚠️  **NOTE** ⚠️<br/>
+⚠️  **NOTE** ⚠️
+
 Outdated contents! This document is kept for historical purpose.
 
 ---
diff --git a/doc/dev/design/ColibriService.md b/doc/dev/design/ColibriService.md
index cf0470504..cc8b7bd27 100644
--- a/doc/dev/design/ColibriService.md
+++ b/doc/dev/design/ColibriService.md
@@ -9,7 +9,8 @@
 * Discussion at: [#3653](https://github.com/scionproto/scion/issues/3653), [#3794](https://github.com/scionproto/scion/issues/3794)
 
 ---
-⚠️  **NOTE** ⚠️<br/>
+⚠️  **NOTE** ⚠️
+
 Outdated contents! This document is kept for historical purpose.
 
 ---
@@ -91,7 +92,7 @@ next AS after AS<sub>i</sub> in the path of the reservation.
 
 1. The *COS* store is queried to admit the segment reservation.
 1. The store decides the admission for the reservation (how much bandwidth). It uses the
-   _traffic_matrix_ from the configuration package.
+   *traffic_matrix* from the configuration package.
 1. The store saves an intermediate reservation entry in the DB.
 1. If this AS is the last one in the path, the *COS* store saves the reservation as final
    and notifies the previous AS in the path with a reservation response.
@@ -105,7 +106,7 @@ ASes. Whenever that configuration changes, the service should be notified.
 1. The service triggers the creation of a new segment reservation at boot time and whenever
    the segment reservation configuration file changes.
 1. The service reads the configuration file and creates a segment reservation request per each entry.
-    * The path used in the request must be obtained using the _path predicate_ in the configuration.
+    * The path used in the request must be obtained using the *path predicate* in the configuration.
 1. The store in the *COS* saves the intermediate request and sends the request to the next AS
    in the path.
 1. If there is a timeout, this store will send a cleanup request to the next AS in the path.
diff --git a/doc/dev/design/EPIC.md b/doc/dev/design/EPIC.md
index 4e69bd503..ae54d988a 100644
--- a/doc/dev/design/EPIC.md
+++ b/doc/dev/design/EPIC.md
@@ -102,7 +102,7 @@ meaningful level of security:
 
 ### Example
 
-![](fig/EPIC/path-type-filtering-small.png).
+![path type filtering figure](fig/EPIC/path-type-filtering-small.png).
 
 Here, AS 6 is the AS protected by the hidden path (blue lines). The
 hidden path terminates at AS 6 (represented by black dot), so AS 6
@@ -182,8 +182,7 @@ not apply any filtering for traffic from interface 2 to interface 1.
 
 There are two main applications for EPIC-HP:
 
-<a id="HighlySecureHiddenPaths"></a>
-### Highly Secure Hidden Paths
+### <a id="HighlySecureHiddenPaths"></a> Highly Secure Hidden Paths
 
 The last and penultimate ASes on the hidden path only allow EPIC-HP
 traffic on the interface pairs that affect the hidden path.
@@ -203,8 +202,7 @@ packets towards hosts in other ASes, but that those hosts can not
 send a response back if they do not have the necessary
 authenticators.
 
-<a id="DOSSecureHiddenPaths"></a>
-### DoS-Secure Hidden Paths
+### <a id="DOSSecureHiddenPaths"></a> DoS-Secure Hidden Paths
 
 The last and penultimate ASes on the hidden path allow EPIC-HP and
 other path types simultaneously, but prioritize traffic using the
@@ -222,9 +220,9 @@ authenticators to send back EPIC-HP traffic.
 
 ## References
 
-<a id="1">[1]</a>
-M. Legner, T. Klenze, M. Wyss, C. Sprenger, A. Perrig. (2020) <br>
-EPIC: Every Packet Is Checked in the Data Plane of a Path-Aware Internet <br>
+[^1]
+M. Legner, T. Klenze, M. Wyss, C. Sprenger, A. Perrig. (2020)\
+EPIC: Every Packet Is Checked in the Data Plane of a Path-Aware Internet\
 Proceedings of the USENIX Security Symposium
 [[Link]](https://netsec.ethz.ch/publications/papers/Legner_Usenix2020_EPIC.pdf)
 
@@ -233,7 +231,7 @@ Design Document for the Hidden Path Infrastructure
 [[Link]](/hidden-paths)
 
 <a id="3">[3]</a>
-T. Lee, C. Pappas, A. Perrig, V. Gligor, and Y. Hu. (2017) <br>
-The Case for In-Network Replay Suppression <br>
+T. Lee, C. Pappas, A. Perrig, V. Gligor, and Y. Hu. (2017)\
+The Case for In-Network Replay Suppression\
 Proceedings of the ACM Asia Conference on Computer and Communications Security
 [[Link]](https://netsec.ethz.ch/publications/papers/replay2017.pdf)
diff --git a/doc/dev/design/PathPolicy.md b/doc/dev/design/PathPolicy.md
index 0e108637a..1d8fa9100 100644
--- a/doc/dev/design/PathPolicy.md
+++ b/doc/dev/design/PathPolicy.md
@@ -42,10 +42,10 @@ Examples:
 
 A policy is defined by a policy object. It can have the following attributes:
 
-- [`extends`](#Extends) (list of extended policies)
-- [`acl`](#ACL) (list of HPs, preceded by `+` or `-`)
-- [`sequence`](#Sequence) (space separated list of HPs, may contain operators)
-- [`options`](#Options) (list of option policies)
+- [`extends`](#extends) (list of extended policies)
+- [`acl`](#acl) (list of HPs, preceded by `+` or `-`)
+- [`sequence`](#sequence) (space separated list of HPs, may contain operators)
+- [`options`](#options) (list of option policies)
     - `weight` (importance level, only valid under `options`)
     - `policy` (a policy object)
 
@@ -75,8 +75,8 @@ Planned:
 - `-` (deny predicate)
 
 The ACL can be used to deny (blacklist) or allow (whitelist) ISDs, ASes and IFs. A deny entry is of
-the following form `- ISD-AS#IF`, where the second part is a [HP](#HP). If a deny entry matches any
-hop on a path, the path is not allowed.
+the following form `- ISD-AS#IF`, where the second part is a [Hop Predicate](#hop-predicate-hp).
+If a deny entry matches any hop on a path, the path is not allowed.
 
 An allow entry uses `+` with a HP, ie. `+ ISD-AS#IF`. For a path to be allowed, every hop of the
 path must be allowed by the ACL. When using allow and deny entries in the same ACL, the first
@@ -113,7 +113,7 @@ Planned:
 - `!` (logical NOT)
 - `&` (logical AND)
 
-The sequence is a string of space separated HPs. The [operators](#Operators) can be used for
+The sequence is a string of space separated HPs. The [operators](#operators) can be used for
 advanced interface sequences.
 
 The following example specifies a path from any interface in AS _1-ff00:0:133_ to two subsequent
@@ -215,7 +215,8 @@ third option which denies only hops in AS _1-ff00:0:133_, is used.
 
 ## Path policies in path lookup
 
-⚠️  **NOTE** ⚠️<br/>
+⚠️  **NOTE** ⚠️
+
 Outdated contents! This section is kept for historical purpose.
 
 ---
diff --git a/doc/dev/design/PathService.md b/doc/dev/design/PathService.md
index ab54dd731..f2b0a4d35 100644
--- a/doc/dev/design/PathService.md
+++ b/doc/dev/design/PathService.md
@@ -4,7 +4,8 @@
 * Status: **outdated**
 
 ---
-⚠️  **NOTE** ⚠️<br/>
+⚠️  **NOTE** ⚠️
+
 Outdated contents! This document is kept for historical purpose.
 
 ---
@@ -15,11 +16,11 @@ The path service is implemented as part of the control server.
 
 We use the messenger to register request handlers. The following requests are handled:
 
-* __Path Registration:__ A handler for the path-registration request, only in core ASes.
-* __Path Requests:__ Two different types of `SegReq` are handled by separate handlers:
+* **Path Registration:** A handler for the path-registration request, only in core ASes.
+* **Path Requests:** Two different types of `SegReq` are handled by separate handlers:
     * local requests, coming over TCP.
     * requests from other path servers, over SCION/QUIC, only in core ASes.
-* __Path Revocation:__ A handler for the path-revocation message
+* **Path Revocation:** A handler for the path-revocation message
 
 ## Deletion of Expired Path Segments and Revocations
 
diff --git a/doc/dev/design/endhost-bootstrap.rst b/doc/dev/design/endhost-bootstrap.rst
index 499d3cc2d..cd040f0c6 100644
--- a/doc/dev/design/endhost-bootstrap.rst
+++ b/doc/dev/design/endhost-bootstrap.rst
@@ -17,14 +17,14 @@ SCION Internet effortlessly.
 From within an AS, all an end user needs to do is to install a package.
 
 In particular, the package contains a bootstrapper daemon that retrieves
-hints from the local network using `zero conf` services, downloads the
+hints from the local network using *zero conf* services, downloads the
 required SCION configuration files from a local discovery server, and starts
 the SCION Daemon (SD).
 
 Terminology
 -----------
 
-A **hint** is a piece of information returned by a `zero conf` service deployed
+A **hint** is a piece of information returned by a *zero conf* service deployed
 in the local network.
 Depending on the discovery mechanism, a hint can be either sufficient to contact
 a discovery server (e.g., providing its IP address) or can be used to query the local
@@ -33,7 +33,7 @@ network further (e.g., a DNS PTR response).
 A **discovery** **server** is a server that exposes the endpoints required
 by the bootstrapper (more in the `Discovery Server`_ section).
 
-A **discoverer** is a client of a `zero conf` service. It communicates with the service
+A **discoverer** is a client of a *zero conf* service. It communicates with the service
 and provides hints to the bootstrapper.
 
 Design
diff --git a/doc/dev/design/forwarding-key-rollover.rst b/doc/dev/design/forwarding-key-rollover.rst
index 426bb1692..84963ef2f 100644
--- a/doc/dev/design/forwarding-key-rollover.rst
+++ b/doc/dev/design/forwarding-key-rollover.rst
@@ -220,7 +220,7 @@ Implementing the router
 
 When the router receives a path for processing, it must verify the HF. The first
 step in this process is computing the MAC verification key. To compute the key,
-first the `ExpirationTime` must be computed:
+first the ``ExpirationTime`` must be computed:
 
 .. code-block:: text
 
diff --git a/doc/dev/design/grpc.rst b/doc/dev/design/grpc.rst
index b01b388e7..8e8aab67c 100644
--- a/doc/dev/design/grpc.rst
+++ b/doc/dev/design/grpc.rst
@@ -71,7 +71,7 @@ By default, gRPC uses the following combination:
 - Resolver: DNS resolver that resolves host names to IP addresses.
 - Balancer: Pick first balancer, that always picks the first address out of the
   list that was provided by the resolver.
-- Dialer: `net.Dialer` with `tcp` from stdlib.
+- Dialer: ``net.Dialer`` with ``tcp`` from stdlib.
 
 Teaching an old dog new tricks |grpc|
 =====================================
diff --git a/doc/dev/git.rst b/doc/dev/git.rst
index c932f2dfc..f3d1243e8 100644
--- a/doc/dev/git.rst
+++ b/doc/dev/git.rst
@@ -126,7 +126,7 @@ Here is an example of a good commit message:
 
    Fixes #3766
 
-- Starts with`<subsystem:>`
+- Starts with ``<subsystem:>``
 - Uses lowercase letters for subject line
 - There is always a reference number to an issue
 
diff --git a/doc/dev/run.rst b/doc/dev/run.rst
index 0de92eb3c..bb7b3f605 100644
--- a/doc/dev/run.rst
+++ b/doc/dev/run.rst
@@ -212,10 +212,12 @@ The basic usage is ``./scion.sh <subcommand> <options>``. The main subcommands a
 end2end_integration
 ===================
 
-:program:bin/end2end_integration is a basic functional test.
+:program:`bin/end2end_integration` is a basic functional test.
 
 The basic usage is ``./end2end_integration <options>``.
 
+.. program:: end2end_integration
+
 .. option:: -d
 
    Assume the SCION services are dockerized. Must be consistent with the last
diff --git a/doc/dev/setup.rst b/doc/dev/setup.rst
index 6d477963e..a3cd52f5a 100644
--- a/doc/dev/setup.rst
+++ b/doc/dev/setup.rst
@@ -76,7 +76,7 @@ Setup
 
       make
 
-   .. hint:: This builds tools for tests in addition to the main SCION services (e.g., `end2end`);
+   .. hint:: This builds tools for tests in addition to the main SCION services (e.g., ``end2end``);
       if you don't require those, you can only build the SCION services by running ``make build``.
       See :doc:`build` for more details.
 
diff --git a/doc/dev/style/go.rst b/doc/dev/style/go.rst
index 773073c3f..efc4063a6 100644
--- a/doc/dev/style/go.rst
+++ b/doc/dev/style/go.rst
@@ -7,7 +7,7 @@ Unless specified otherwise below, stick to golang's
 
 Generally the code should be formatted with ``gofmt`` (checked by CI).
 
-Lines must be at most 100 characters long (checked by CI via `lll`).
+Lines must be at most 100 characters long (checked by CI via ``lll``).
 
 Naming
 ------
@@ -138,10 +138,10 @@ In cases where performance is a concern, consider applying the labels outside of
 the performance-critical section.
 
 .. note::
-   Some packages have `metrics` packages that define labels and initialize
+   Some packages have ``metrics`` packages that define labels and initialize
    metrics (see the ``go/cs/beacon/metrics`` package for an example). While this
    is also ok, the recommended way is to define labels in the package itself and
-   initialize metrics in `main`.
+   initialize metrics in ``main``.
 
 Best Practices
 ^^^^^^^^^^^^^^
@@ -152,4 +152,4 @@ Best Practices
 #. Use values that can be searched with regex. E.g. prepend ``err_`` for every error result.
 #. ``snake_case`` label names and values.
 #. Put shared label names and values into ``go/lib/prom``.
-#. Always initialize ``CounterVec`` to avoid hidden metrics `link <https://prometheus.io/docs/practices/instrumentation/#avoid-missing-metrics)>`.
+#. Always initialize ``CounterVec`` to avoid hidden metrics `link <https://prometheus.io/docs/practices/instrumentation/#avoid-missing-metrics)>`_.
diff --git a/doc/lint.bzl b/doc/lint.bzl
new file mode 100644
index 000000000..ee618dad7
--- /dev/null
+++ b/doc/lint.bzl
@@ -0,0 +1,41 @@
+def _sphinx_lint_test_impl(ctx):
+    args = list(ctx.attr.args)
+    for s in ctx.files.srcs:
+        args.append(s.short_path)
+
+    out = ctx.actions.declare_file(ctx.label.name + "_exec")
+    ctx.actions.write(
+        output = out,
+        content = """
+        #!/usr/bin/env bash
+        exec {bin} {args}
+        """.format(
+            bin = ctx.executable._sphinx_lint_cli.short_path,
+            args = " ".join(args),
+        ),
+    )
+
+    deps = ctx.attr._sphinx_lint_cli.default_runfiles.files.to_list()
+
+    runfiles = ctx.runfiles(
+        files = ctx.files.srcs + deps,
+    )
+    return [
+        DefaultInfo(
+            executable = out,
+            runfiles = runfiles,
+        ),
+    ]
+
+sphinx_lint_test = rule(
+    implementation = _sphinx_lint_test_impl,
+    attrs = {
+        "srcs": attr.label_list(allow_files = True),
+        "_sphinx_lint_cli": attr.label(
+            cfg = "host",
+            default = "//doc:sphinx-lint",
+            executable = True,
+        ),
+    },
+    test = True,
+)
diff --git a/doc/manuals/control.rst b/doc/manuals/control.rst
index 5855abfd1..11035f2e0 100644
--- a/doc/manuals/control.rst
+++ b/doc/manuals/control.rst
@@ -252,7 +252,7 @@ considers the following options.
          - use the `netsys-lab/scion-ca <https://github.com/netsys-lab/scion-ca>`_ SCION CA
            based on `smallstep's step-ca <https://github.com/smallstep/certificates>`_,
          - ask SCION vendors for proprietary CA implementations and offerings,
-         - plug in your own CA service implementing the :file-ref:`spec/ca.gen.yaml` API.
+         - plug in your own CA service implementing the :file-ref:`spec/ca.gen.yml` API.
 
       .. option:: ca.service.address = <string>
 
@@ -262,7 +262,7 @@ considers the following options.
 
       .. option:: ca.service.shared_secret = <string>
 
-	      Path to the PEM-encoded shared secret that is used to create JWT tokens.
+         Path to the PEM-encoded shared secret that is used to create JWT tokens.
 
          The shared secret file is re-read from disk at 5 second intervals.
 
diff --git a/doc/manuals/gateway/metrics.rst b/doc/manuals/gateway/metrics.rst
index f8a51fd3d..4ac1fb536 100644
--- a/doc/manuals/gateway/metrics.rst
+++ b/doc/manuals/gateway/metrics.rst
@@ -227,7 +227,7 @@ Session is healthy
 
 **Description**: Healthiness flag to a remote AS per session ID. The
 session is ephemeral so it is recommended to use after aggregating
-per `remote_isd_as` and `policy_id`.
+per ``remote_isd_as`` and ``policy_id``.
 
 **Labels**: ``remote_isd_as``, ``policy_id``, ``session_id``
 
diff --git a/doc/manuals/router.rst b/doc/manuals/router.rst
index 9dd5d462a..def129e51 100644
--- a/doc/manuals/router.rst
+++ b/doc/manuals/router.rst
@@ -192,7 +192,7 @@ considers the following options.
 
          Can be overridden for specific inter-AS BFD
          sessions with :option:`bfd.disable <topology-json disable>`.
- 
+
       .. option:: detect_mult = <uint8>, default 3
 
          Set the :term:`BFD` detection time multiplier.
diff --git a/doc/protocols/bfd.rst b/doc/protocols/bfd.rst
index c7b850b70..ce2cfa804 100644
--- a/doc/protocols/bfd.rst
+++ b/doc/protocols/bfd.rst
@@ -27,7 +27,7 @@ directly into SCION payload, with no additional intermediate protocol::
     |         BFD          |
     +----------------------+
 
-The `NextHdr` field in the SCION common header must be set to type `BFD` (17).
+The ``NextHdr`` field in the SCION common header must be set to type ``BFD`` (17).
 
 BFD in SCION Router
 ===================
@@ -40,7 +40,7 @@ SCION router should choose its discriminators for BFD sessions at random.
 Bootstrapping
 -------------
 
-BFD bootstrapping process (that is, how incoming BFD packet with `Your Discriminator`
+BFD bootstrapping process (that is, how incoming BFD packet with ``Your Discriminator``
 field equal to zero is mapped to a BFD session) is to be defined by each
 particular application.
 
diff --git a/doc/protocols/scion-header.rst b/doc/protocols/scion-header.rst
index c69093f96..f42703301 100644
--- a/doc/protocols/scion-header.rst
+++ b/doc/protocols/scion-header.rst
@@ -146,7 +146,7 @@ CurrHF
     calculations below).
 Seg{0,1,2}Len
     The number of hop fields in a given segment. :math:`Seg_iLen > 0` implies
-    the existence of info field `i`.
+    the existence of info field :math:`i`.
 
 Path Offset Calculations
 ^^^^^^^^^^^^^^^^^^^^^^^^
@@ -277,7 +277,7 @@ To that end, MACs are calculated over the relevant fields of a hop field and
 additionally (conceptually) chained to other hop fields in the path segment. In
 the following, we specify the computation of a hop field MAC.
 
-We write the `i`-th  hop field in a path segment (in construction direction) as
+We write the :math:`i`-th  hop field in a path segment (in construction direction) as
 
 .. math::
     HF_i = \langle  Flags_i || ExpTime_i || InIF_i || EgIF_i || \sigma_i \rangle
@@ -340,7 +340,7 @@ Path Calculation
 **Initialization**
 
 The paths must be initialized correctly for the border routers to verify the hop
-fields in the data plane. `SegID` is an updatable field and is initialized based
+fields in the data plane. :math:`SegID` is an updatable field and is initialized based
 on the location of sender in relation to path construction.
 
 
@@ -348,50 +348,50 @@ on the location of sender in relation to path construction.
 Initialization cases:
 
 - The non-peering path segment is traversed in construction direction. It starts
-  at the `i`-th AS of the full segment discovered in beaconing:
+  at the :math:`i`-th AS of the full segment discovered in beaconing:
 
   :math:`SegID := \beta_{i}`
 
 - The peering path segment is traversed in construction direction. It starts at
-  the `i`-th AS of the full segment discovered in beaconing:
+  the :math:`i`-th AS of the full segment discovered in beaconing:
 
   :math:`SegID := \beta_{i+1}`
 
 - The path segment is traversed against construction direction. The full segment
-  discovered in beaconing has `n` hops:
+  discovered in beaconing has :math:`n` hops:
 
   :math:`SegID := \beta_{n}`
 
 **AS Traversal Operations**
 
 Each AS on the path verifies the hop fields with the help of the current value
-in `SegID`. The operations differ based on the location of the AS on the path.
-Each AS has to set the `SegID` correctly for the next AS to verify its hop
+in :math:`SegID`. The operations differ based on the location of the AS on the path.
+Each AS has to set the :math:`SegID` correctly for the next AS to verify its hop
 field.
 
-Each operation is described form the perspective of AS `i`.
+Each operation is described form the perspective of AS :math:`i`.
 
 Against construction direction (up, i.e., ConsDir == 0):
-   #. `SegID` contains :math:`\beta_{i+1}` at this point.
+   #. :math:`SegID` contains :math:`\beta_{i+1}` at this point.
    #. Compute :math:`\beta'_{i} := SegID \oplus \sigma_i[:2]`
-   #. At the ingress router update `SegID`:
+   #. At the ingress router update :math:`SegID`:
 
       :math:`SegID := \beta'_{i}`
-   #. `SegID` now contains :math:`\beta'_{i}`
+   #. :math:`SegID` now contains :math:`\beta'_{i}`
    #. Compute :math:`\sigma_i` with the formula above by replacing
       :math:`\beta_{i}` with :math:`SegID`.
    #. Check that the MAC in the hop field matches :math:`\sigma_{i}`. If the
       MAC matches it follows that :math:`\beta'_{i} == \beta_{i}`.
 
 In construction direction (down, i.e., ConsDir == 1):
-   #. `SegID` contains :math:`\beta_{i}` at this point.
+   #. :math:`SegID` contains :math:`\beta_{i}` at this point.
    #. Compute :math:`\sigma'_i` with the formula above by replacing
-      :math:`\beta_{i}` with `SegID`.
+      :math:`\beta_{i}` with :math:`SegID`.
    #. Check that the MAC in the hop field matches :math:`\sigma'_{i}`.
-   #. At the egress router update `SegID` for the next hop:
+   #. At the egress router update :math:`SegID` for the next hop:
 
       :math:`SegID := SegID \oplus \sigma_i[:2]`
-   #. `SegID` now contains :math:`\beta_{i+1}`.
+   #. :math:`SegID` now contains :math:`\beta_{i+1}`.
 
 An example of how processing is done in up and down direction is shown in the
 illustration below:
@@ -406,18 +406,18 @@ the first hop of the segment. Against construction direction (up), the peering
 AS is the last hop of the segment.
 
 Against construction direction (up):
-   #. `SegID` contains :math:`\beta_{i+1}` at this point.
+   #. :math:`SegID` contains :math:`\beta_{i+1}` at this point.
    #. Compute :math:`{\sigma^P_i}'` with the formula above by replacing
-      :math:`\beta_{i+1}` with `SegID`.
+      :math:`\beta_{i+1}` with :math:`SegID`.
    #. Check that the MAC in the hop field matches :math:`{\sigma^P_i}'`.
-   #. Do not update `SegID` as it already contains :math:`\beta_{i+1}`.
+   #. Do not update :math:`SegID` as it already contains :math:`\beta_{i+1}`.
 
 In construction direction (down):
-   #. `SegID` contains :math:`\beta_{i+1}` at this point.
+   #. :math:`SegID` contains :math:`\beta_{i+1}` at this point.
    #. Compute :math:`{\sigma^P_i}'` with the formula above by replacing
-      :math:`\beta_{i+1}` with `SegID`.
+      :math:`\beta_{i+1}` with :math:`SegID`.
    #. Check that the MAC in the hop field matches :math:`{\sigma^P_i}'`.
-   #. Do not update `SegID` as it already contains :math:`\beta_{i+1}`.
+   #. Do not update :math:`SegID` as it already contains :math:`\beta_{i+1}`.
 
 .. _path-type-empty:
 
diff --git a/doc/requirements.in b/doc/requirements.in
index bbac42f58..3e47a367e 100644
--- a/doc/requirements.in
+++ b/doc/requirements.in
@@ -4,3 +4,4 @@ sphinx-autobuild
 sphinx-rtd-theme
 sphinxcontrib-openapi
 sphinx_copybutton
+sphinx-lint
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 651c7a858..53a7e32cc 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,8 +1,8 @@
 #
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --generate-hashes --resolver=backtracking requirements.in
+#    bazel run //doc:requirements.update
 #
 alabaster==0.7.13 \
     --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
@@ -138,14 +138,6 @@ imagesize==1.4.1 \
     --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
     --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
     # via sphinx
-importlib-metadata==6.0.0 \
-    --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
-    --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d
-    # via sphinx
-importlib-resources==5.12.0 \
-    --hash=sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6 \
-    --hash=sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a
-    # via jsonschema
 jinja2==3.1.2 \
     --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
     --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
@@ -222,10 +214,10 @@ picobox==2.2.0 \
     --hash=sha256:0a14f8fae386a46e61671a3ce16e0d54d24fb8f15169b0971483228a3e274622 \
     --hash=sha256:c446ea34b5ee396ab8dc7cbed8f036e73e52403593088656b9b45b7d6382c926
     # via sphinxcontrib-openapi
-pkgutil-resolve-name==1.3.10 \
-    --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \
-    --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e
-    # via jsonschema
+polib==1.2.0 \
+    --hash=sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d \
+    --hash=sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b
+    # via sphinx-lint
 pygments==2.14.0 \
     --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
     --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
@@ -310,7 +302,102 @@ pyyaml==6.0 \
 recommonmark==0.7.1 \
     --hash=sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f \
     --hash=sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67
-    # via -r requirements.in
+    # via -r doc/requirements.in
+regex==2023.12.25 \
+    --hash=sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5 \
+    --hash=sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770 \
+    --hash=sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc \
+    --hash=sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105 \
+    --hash=sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d \
+    --hash=sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b \
+    --hash=sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9 \
+    --hash=sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630 \
+    --hash=sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6 \
+    --hash=sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c \
+    --hash=sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482 \
+    --hash=sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6 \
+    --hash=sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a \
+    --hash=sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80 \
+    --hash=sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5 \
+    --hash=sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1 \
+    --hash=sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f \
+    --hash=sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf \
+    --hash=sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb \
+    --hash=sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2 \
+    --hash=sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347 \
+    --hash=sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20 \
+    --hash=sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060 \
+    --hash=sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5 \
+    --hash=sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73 \
+    --hash=sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f \
+    --hash=sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d \
+    --hash=sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3 \
+    --hash=sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae \
+    --hash=sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4 \
+    --hash=sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2 \
+    --hash=sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457 \
+    --hash=sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c \
+    --hash=sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4 \
+    --hash=sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87 \
+    --hash=sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0 \
+    --hash=sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704 \
+    --hash=sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f \
+    --hash=sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f \
+    --hash=sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b \
+    --hash=sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5 \
+    --hash=sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923 \
+    --hash=sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715 \
+    --hash=sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c \
+    --hash=sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca \
+    --hash=sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1 \
+    --hash=sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756 \
+    --hash=sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360 \
+    --hash=sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc \
+    --hash=sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445 \
+    --hash=sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e \
+    --hash=sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4 \
+    --hash=sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a \
+    --hash=sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8 \
+    --hash=sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53 \
+    --hash=sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697 \
+    --hash=sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf \
+    --hash=sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a \
+    --hash=sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415 \
+    --hash=sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f \
+    --hash=sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9 \
+    --hash=sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400 \
+    --hash=sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d \
+    --hash=sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392 \
+    --hash=sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb \
+    --hash=sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd \
+    --hash=sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861 \
+    --hash=sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232 \
+    --hash=sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95 \
+    --hash=sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7 \
+    --hash=sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39 \
+    --hash=sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887 \
+    --hash=sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5 \
+    --hash=sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39 \
+    --hash=sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb \
+    --hash=sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586 \
+    --hash=sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97 \
+    --hash=sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423 \
+    --hash=sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69 \
+    --hash=sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7 \
+    --hash=sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1 \
+    --hash=sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7 \
+    --hash=sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5 \
+    --hash=sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8 \
+    --hash=sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91 \
+    --hash=sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590 \
+    --hash=sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe \
+    --hash=sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c \
+    --hash=sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64 \
+    --hash=sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd \
+    --hash=sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa \
+    --hash=sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31 \
+    --hash=sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988
+    # via sphinx-lint
 requests==2.28.2 \
     --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \
     --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf
@@ -329,7 +416,7 @@ sphinx==6.1.3 \
     --hash=sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2 \
     --hash=sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc
     # via
-    #   -r requirements.in
+    #   -r doc/requirements.in
     #   recommonmark
     #   sphinx-autobuild
     #   sphinx-copybutton
@@ -339,11 +426,15 @@ sphinx==6.1.3 \
 sphinx-autobuild==2021.3.14 \
     --hash=sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac \
     --hash=sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05
+    # via -r doc/requirements.in
 sphinx-copybutton==0.5.2 \
     --hash=sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd \
     --hash=sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e
-    # via -r requirements.in
-    # via -r requirements.in
+    # via -r doc/requirements.in
+sphinx-lint==0.9.1 \
+    --hash=sha256:185cee19ff1129549c45e15a3b25404daeb47c54d15112dda589cedad82957aa \
+    --hash=sha256:df34271ab65ce43676cbd90726f4dea5cd200b43b01448b2aee8f06e609edcbb
+    # via -r doc/requirements.in
 sphinx-mdinclude==0.5.3 \
     --hash=sha256:02afadf4597aecf8255a702956eff5b8c5cb9658ea995c3d361722d2ed78cca9 \
     --hash=sha256:2998e3d18b3022c9983d1b72191fe37e25ffccd54165cbe3acb22cceedd91af4
@@ -351,7 +442,7 @@ sphinx-mdinclude==0.5.3 \
 sphinx-rtd-theme==1.2.0 \
     --hash=sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8 \
     --hash=sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2
-    # via -r requirements.in
+    # via -r doc/requirements.in
 sphinxcontrib-applehelp==1.0.4 \
     --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
     --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
@@ -379,7 +470,7 @@ sphinxcontrib-jsmath==1.0.1 \
 sphinxcontrib-openapi==0.8.1 \
     --hash=sha256:04fcf87c259345162a504ce3dfee0f71389f507c3797798dc531c7748995b4eb \
     --hash=sha256:45ecad2a98e304d3665c133ea08a7083a2b5d86e75c6b64c60389240b3927ad5
-    # via -r requirements.in
+    # via -r doc/requirements.in
 sphinxcontrib-qthelp==1.0.3 \
     --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
     --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
@@ -405,13 +496,9 @@ urllib3==1.26.14 \
     --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \
     --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1
     # via requests
-zipp==3.14.0 \
-    --hash=sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7 \
-    --hash=sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb
-    # via
-    #   importlib-metadata
-    #   importlib-resources
 
-# WARNING: The following packages were not pinned, but pip requires them to be
-# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
-# setuptools
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==69.2.0 \
+    --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \
+    --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c
+    # via sphinxcontrib-jquery
diff --git a/doc/tutorials/deploy.rst b/doc/tutorials/deploy.rst
index 7d4c0d157..25ec9a8ee 100644
--- a/doc/tutorials/deploy.rst
+++ b/doc/tutorials/deploy.rst
@@ -247,7 +247,7 @@ In practice, the private keys of ASes are of course never revealed to other enti
 
    .. note::
 
-      The script above will generate a new TRC for your ISD and must be done exactly once. Once you deploy such TRC on your machines, further TRC updates must be sequential. If for any reason you need to reset your setup and you need to deploy a fresh new TRC generated with the script above, then you must first delete the local DB files on your hosts (in `/var/lib/scion/`).
+      The script above will generate a new TRC for your ISD and must be done exactly once. Once you deploy such TRC on your machines, further TRC updates must be sequential. If for any reason you need to reset your setup and you need to deploy a fresh new TRC generated with the script above, then you must first delete the local DB files on your hosts (in ``/var/lib/scion/``).
 
 
 #. The just-generated crypto material in can now be copied to the respective AS VMs.
diff --git a/tools/md/Dockerfile b/tools/md/Dockerfile
deleted file mode 100644
index 9dda870a7..000000000
--- a/tools/md/Dockerfile
+++ /dev/null
@@ -1,12 +0,0 @@
-FROM alpine:3.10
-
-RUN apk update && \
-    apk add --no-cache ruby ruby-json && \
-    gem install --no-rdoc --no-ri -v 0.9.0 mdl
-
-RUN mkdir /data
-
-WORKDIR /data
-
-ENTRYPOINT ["mdl"]
-CMD ["--help"]
diff --git a/tools/md/mdlintstyle.rb b/tools/md/mdlintstyle.rb
deleted file mode 100644
index 4e6114f50..000000000
--- a/tools/md/mdlintstyle.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-all
-rule 'MD013', :line_length => 100
-rule 'MD013', :code_blocks => false
-rule 'MD013', :tables => false
-rule 'MD007', :indent => 4 # see https://github.com/markdownlint/markdownlint/issues/139
-rule 'MD024', :allow_different_nesting => true
-exclude_rule 'MD033'
diff --git a/tools/md/skipped b/tools/md/skipped
deleted file mode 100644
index 3e1c5d227..000000000
--- a/tools/md/skipped
+++ /dev/null
@@ -1,4 +0,0 @@
-^\./docker/_build/
-^\./licenses/data
-^\./rules_openapi/tools/
-^\./tools/coremark/LICENSE.md


Test patch:
diff --git a/doc/dev/testing/buildkite.rst b/doc/dev/testing/buildkite.rst
index 195afed73..cebb56a1e 100644
--- a/doc/dev/testing/buildkite.rst
+++ b/doc/dev/testing/buildkite.rst
@@ -44,7 +44,7 @@ Agents
 Machine Image
 -------------
 
-We use the default machine image of the *Elastic CI Stack for AWS* release `v6.7.1 <https://github.com/buildkite/elastic-ci-stack-for-aws/releases/tag/v6.7.1>`.
+We use the default machine image of the *Elastic CI Stack for AWS* release `v6.7.1 <https://github.com/buildkite/elastic-ci-stack-for-aws/releases/tag/v6.7.1>`_.
 `What's on each machine <https://buildkite.com/docs/agent/v3/elastic-ci-aws#before-you-start-whats-on-each-machine>`_:
 
 - Amazon Linux 2023