diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml
index 278eb20..3846726 100644
--- a/.github/workflows/test-devel.yml
+++ b/.github/workflows/test-devel.yml
@@ -156,6 +156,46 @@ jobs:
         name: installed-inspircd
         path: ~/artefacts-*.tar.gz
         retention-days: 1
+  build-ngircd:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Create directories
+      run: cd ~/; mkdir -p .local/ go/
+    - name: Cache dependencies
+      uses: actions/cache@v2
+      with:
+        key: 3-${{ runner.os }}-ngircd-devel
+        path: '~/.cache
+
+          ${ github.workspace }/ngircd
+
+          '
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Checkout ngircd
+      uses: actions/checkout@v2
+      with:
+        path: ngircd
+        ref: master
+        repository: ngircd/ngircd
+    - name: Build ngircd
+      run: |
+        cd $GITHUB_WORKSPACE/ngircd
+        ./autogen.sh
+        ./configure --prefix=$HOME/.local/
+        make -j 4
+        make install
+    - name: Make artefact tarball
+      run: cd ~; tar -czf artefacts-ngircd.tar.gz .local/ go/
+    - name: Upload build artefacts
+      uses: actions/upload-artifact@v2
+      with:
+        name: installed-ngircd
+        path: ~/artefacts-*.tar.gz
+        retention-days: 1
   build-plexus4:
     runs-on: ubuntu-latest
     steps:
@@ -295,6 +335,9 @@ jobs:
     - test-inspircd-anope
     - test-ircu2
     - test-limnoria
+    - test-ngircd
+    - test-ngircd-anope
+    - test-ngircd-atheme
     - test-plexus4
     - test-solanum
     - test-sopel
@@ -444,7 +487,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/go/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/go/sbin:~/go/bin:$PATH
         make ergo
     - if: always()
       name: Publish results
@@ -508,7 +551,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
         make inspircd
     - if: always()
       name: Publish results
@@ -546,7 +589,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH  make
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH  make
         inspircd-anope
     - if: always()
       name: Publish results
@@ -619,6 +662,108 @@ jobs:
       with:
         name: pytest results limnoria (devel)
         path: pytest.xml
+  test-ngircd:
+    needs:
+    - build-ngircd
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-ngircd
+        path: '~'
+    - name: Unpack artefacts
+      run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
+    - name: Install Atheme
+      run: sudo apt-get install atheme-services
+    - name: Install irctest dependencies
+      run: |-
+        python -m pip install --upgrade pip
+        pip install pytest pytest-xdist -r requirements.txt
+    - name: Test with pytest
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local//sbin:~/.local//bin:$PATH
+        make ngircd
+    - if: always()
+      name: Publish results
+      uses: actions/upload-artifact@v2
+      with:
+        name: pytest results ngircd (devel)
+        path: pytest.xml
+  test-ngircd-anope:
+    needs:
+    - build-ngircd
+    - build-anope
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-ngircd
+        path: '~'
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-anope
+        path: '~'
+    - name: Unpack artefacts
+      run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
+    - name: Install Atheme
+      run: sudo apt-get install atheme-services
+    - name: Install irctest dependencies
+      run: |-
+        python -m pip install --upgrade pip
+        pip install pytest pytest-xdist -r requirements.txt
+    - name: Test with pytest
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local//sbin:~/.local//bin:$PATH  make
+        ngircd-anope
+    - if: always()
+      name: Publish results
+      uses: actions/upload-artifact@v2
+      with:
+        name: pytest results ngircd-anope (devel)
+        path: pytest.xml
+  test-ngircd-atheme:
+    needs:
+    - build-ngircd
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-ngircd
+        path: '~'
+    - name: Unpack artefacts
+      run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
+    - name: Install Atheme
+      run: sudo apt-get install atheme-services
+    - name: Install irctest dependencies
+      run: |-
+        python -m pip install --upgrade pip
+        pip install pytest pytest-xdist -r requirements.txt
+    - name: Test with pytest
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local//sbin:~/.local//bin:$PATH
+        make ngircd-atheme
+    - if: always()
+      name: Publish results
+      uses: actions/upload-artifact@v2
+      with:
+        name: pytest results ngircd-atheme (devel)
+        path: pytest.xml
   test-plexus4:
     needs:
     - build-plexus4
@@ -739,7 +884,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
         make unrealircd
     - if: always()
       name: Publish results
@@ -777,7 +922,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/bin:$PATH  make
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH  make
         unrealircd-anope
     - if: always()
       name: Publish results
@@ -809,7 +954,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
         make unrealircd-atheme
     - if: always()
       name: Publish results
diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml
index 185ef23..d91a5c5 100644
--- a/.github/workflows/test-devel_release.yml
+++ b/.github/workflows/test-devel_release.yml
@@ -110,7 +110,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
         make inspircd
     - if: always()
       name: Publish results
@@ -148,7 +148,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH  make
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH  make
         inspircd-anope
     - if: always()
       name: Publish results
@@ -180,7 +180,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
         make inspircd-atheme
     - if: always()
       name: Publish results
diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml
index d092ca0..5d2b654 100644
--- a/.github/workflows/test-stable.yml
+++ b/.github/workflows/test-stable.yml
@@ -196,6 +196,46 @@ jobs:
         name: installed-inspircd
         path: ~/artefacts-*.tar.gz
         retention-days: 1
+  build-ngircd:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Create directories
+      run: cd ~/; mkdir -p .local/ go/
+    - name: Cache dependencies
+      uses: actions/cache@v2
+      with:
+        key: 3-${{ runner.os }}-ngircd-stable
+        path: '~/.cache
+
+          ${ github.workspace }/ngircd
+
+          '
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Checkout ngircd
+      uses: actions/checkout@v2
+      with:
+        path: ngircd
+        ref: rel-26.1
+        repository: ngircd/ngircd
+    - name: Build ngircd
+      run: |
+        cd $GITHUB_WORKSPACE/ngircd
+        ./autogen.sh
+        ./configure --prefix=$HOME/.local/
+        make -j 4
+        make install
+    - name: Make artefact tarball
+      run: cd ~; tar -czf artefacts-ngircd.tar.gz .local/ go/
+    - name: Upload build artefacts
+      uses: actions/upload-artifact@v2
+      with:
+        name: installed-ngircd
+        path: ~/artefacts-*.tar.gz
+        retention-days: 1
   build-plexus4:
     runs-on: ubuntu-latest
     steps:
@@ -338,6 +378,9 @@ jobs:
     - test-irc2
     - test-ircu2
     - test-limnoria
+    - test-ngircd
+    - test-ngircd-anope
+    - test-ngircd-atheme
     - test-plexus4
     - test-solanum
     - test-sopel
@@ -519,7 +562,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/go/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/go/sbin:~/go/bin:$PATH
         make ergo
     - if: always()
       name: Publish results
@@ -583,7 +626,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
         make inspircd
     - if: always()
       name: Publish results
@@ -621,7 +664,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH  make
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH  make
         inspircd-anope
     - if: always()
       name: Publish results
@@ -653,7 +696,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
         make inspircd-atheme
     - if: always()
       name: Publish results
@@ -779,6 +822,108 @@ jobs:
       with:
         name: pytest results limnoria (stable)
         path: pytest.xml
+  test-ngircd:
+    needs:
+    - build-ngircd
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-ngircd
+        path: '~'
+    - name: Unpack artefacts
+      run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
+    - name: Install Atheme
+      run: sudo apt-get install atheme-services
+    - name: Install irctest dependencies
+      run: |-
+        python -m pip install --upgrade pip
+        pip install pytest pytest-xdist -r requirements.txt
+    - name: Test with pytest
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local//sbin:~/.local//bin:$PATH
+        make ngircd
+    - if: always()
+      name: Publish results
+      uses: actions/upload-artifact@v2
+      with:
+        name: pytest results ngircd (stable)
+        path: pytest.xml
+  test-ngircd-anope:
+    needs:
+    - build-ngircd
+    - build-anope
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-ngircd
+        path: '~'
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-anope
+        path: '~'
+    - name: Unpack artefacts
+      run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
+    - name: Install Atheme
+      run: sudo apt-get install atheme-services
+    - name: Install irctest dependencies
+      run: |-
+        python -m pip install --upgrade pip
+        pip install pytest pytest-xdist -r requirements.txt
+    - name: Test with pytest
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local//sbin:~/.local//bin:$PATH  make
+        ngircd-anope
+    - if: always()
+      name: Publish results
+      uses: actions/upload-artifact@v2
+      with:
+        name: pytest results ngircd-anope (stable)
+        path: pytest.xml
+  test-ngircd-atheme:
+    needs:
+    - build-ngircd
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: Download build artefacts
+      uses: actions/download-artifact@v2
+      with:
+        name: installed-ngircd
+        path: '~'
+    - name: Unpack artefacts
+      run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
+    - name: Install Atheme
+      run: sudo apt-get install atheme-services
+    - name: Install irctest dependencies
+      run: |-
+        python -m pip install --upgrade pip
+        pip install pytest pytest-xdist -r requirements.txt
+    - name: Test with pytest
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local//sbin:~/.local//bin:$PATH
+        make ngircd-atheme
+    - if: always()
+      name: Publish results
+      uses: actions/upload-artifact@v2
+      with:
+        name: pytest results ngircd-atheme (stable)
+        path: pytest.xml
   test-plexus4:
     needs:
     - build-plexus4
@@ -899,7 +1044,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
         make unrealircd
     - if: always()
       name: Publish results
@@ -937,7 +1082,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/bin:$PATH  make
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH  make
         unrealircd-anope
     - if: always()
       name: Publish results
@@ -969,7 +1114,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install pytest pytest-xdist -r requirements.txt
     - name: Test with pytest
-      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/bin:$PATH
+      run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH  PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
         make unrealircd-atheme
     - if: always()
       name: Publish results
diff --git a/Makefile b/Makefile
index 91395c2..7f8f409 100644
--- a/Makefile
+++ b/Makefile
@@ -104,6 +104,20 @@ MAMMON_SELECTORS := \
 	and not strict \
 	$(EXTRA_SELECTORS)
 
+# testKeyValidation[spaces] and testKeyValidation[empty] fail because ngIRCd does not validate them https://github.com/ngircd/ngircd/issues/290
+# testStarNick: wat
+# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS.
+# chathistory tests fail because they need nicks longer than 9 chars
+NGIRCD_SELECTORS := \
+	not Ergo \
+	and not deprecated \
+	and not strict \
+	and not (testKeyValidation and (spaces or empty)) \
+	and not testStarNick \
+	and not testEmptyRealname \
+	and not chathistory \
+	$(EXTRA_SELECTORS)
+
 # testInviteUnoppedModern is the only strict test that Plexus4 fails
 # testInviteInviteOnlyModern fails because Plexus4 allows non-op to invite if (and only if) the channel is not invite-only
 PLEXUS4_SELECTORS := \
@@ -262,6 +276,27 @@ plexus4:
 		-m 'not services' \
 		-k "$(PLEXUS4_SELECTORS)"
 
+ngircd:
+	$(PYTEST) $(PYTEST_ARGS) \
+		--controller irctest.controllers.ngircd \
+		-m 'not services' \
+		-n 10 \
+		-k "$(NGIRCD_SELECTORS)"
+
+ngircd-anope:
+	$(PYTEST) $(PYTEST_ARGS) \
+		--controller irctest.controllers.ngircd \
+		--services-controller=irctest.controllers.anope_services \
+		-m 'services' \
+		-k "$(NGIRCD_SELECTORS)"
+
+ngircd-atheme:
+	$(PYTEST) $(PYTEST_ARGS) \
+		--controller irctest.controllers.ngircd \
+		--services-controller=irctest.controllers.atheme_services \
+		-m 'services' \
+		-k "$(NGIRCD_SELECTORS)"
+
 solanum:
 	$(PYTEST) $(PYTEST_ARGS) \
 		--controller=irctest.controllers.solanum \
diff --git a/irctest/controllers/anope_services.py b/irctest/controllers/anope_services.py
index 3ccdb29..f620dd9 100644
--- a/irctest/controllers/anope_services.py
+++ b/irctest/controllers/anope_services.py
@@ -83,6 +83,7 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
             "hybrid",
             "plexus",
             "unreal4",
+            "ngircd",
         )
 
         with self.open_file("conf/services.conf") as fd:
diff --git a/irctest/controllers/atheme_services.py b/irctest/controllers/atheme_services.py
index 6877c9f..8994677 100644
--- a/irctest/controllers/atheme_services.py
+++ b/irctest/controllers/atheme_services.py
@@ -17,6 +17,7 @@ loadmodule "modules/nickserv/cert";
 loadmodule "modules/nickserv/register";
 loadmodule "modules/nickserv/verify";
 
+loadmodule "modules/saslserv/main";
 loadmodule "modules/saslserv/authcookie";
 #loadmodule "modules/saslserv/ecdh-x25519-challenge";
 loadmodule "modules/saslserv/ecdsa-nist256p-challenge";
@@ -61,7 +62,7 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
         if protocol == "inspircd3":
             # That's the name used by Anope
             protocol = "inspircd"
-        assert protocol in ("bahamut", "inspircd", "charybdis", "unreal4")
+        assert protocol in ("bahamut", "inspircd", "charybdis", "unreal4", "ngircd")
 
         with self.open_file("services.conf") as fd:
             fd.write(
diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py
new file mode 100644
index 0000000..5d0d845
--- /dev/null
+++ b/irctest/controllers/ngircd.py
@@ -0,0 +1,117 @@
+import os
+import subprocess
+from typing import Optional, Set, Type
+
+from irctest.basecontrollers import (
+    BaseServerController,
+    DirectoryBasedController,
+    NotImplementedByController,
+)
+from irctest.irc_utils.junkdrawer import find_hostname_and_port
+
+TEMPLATE_CONFIG = """
+[Global]
+    Name = My.Little.Server
+    Info = ExampleNET Server
+    Bind = {hostname}
+    Ports = {port}
+    AdminInfo1 = Bob Smith
+    AdminEMail = email@example.org
+    {password_field}
+
+[Server]
+    Name = services.example.org
+    MyPassword = password
+    PeerPassword = password
+    Passive = yes  # don't connect to it
+    ServiceMask = *Serv
+
+[Operator]
+    Name = operuser
+    Password = operpassword
+"""
+
+
+class NgircdController(BaseServerController, DirectoryBasedController):
+    software_name = "ngIRCd"
+    supported_sasl_mechanisms: Set[str] = set()
+    supports_sts = False
+
+    def create_config(self) -> None:
+        super().create_config()
+        with self.open_file("server.conf"):
+            pass
+
+    def run(
+        self,
+        hostname: str,
+        port: int,
+        *,
+        password: Optional[str],
+        ssl: bool,
+        run_services: bool,
+        valid_metadata_keys: Optional[Set[str]] = None,
+        invalid_metadata_keys: Optional[Set[str]] = None,
+        restricted_metadata_keys: Optional[Set[str]] = None,
+    ) -> None:
+        if valid_metadata_keys or invalid_metadata_keys:
+            raise NotImplementedByController(
+                "Defining valid and invalid METADATA keys."
+            )
+        assert self.proc is None
+        self.port = port
+        self.hostname = hostname
+        self.create_config()
+        (unused_hostname, unused_port) = find_hostname_and_port()
+
+        password_field = "Password = {}".format(password) if password else ""
+
+        self.gen_ssl()
+        if ssl:
+            (tls_hostname, tls_port) = (hostname, port)
+            (hostname, port) = (unused_hostname, unused_port)
+        else:
+            # Unreal refuses to start without TLS enabled
+            (tls_hostname, tls_port) = (unused_hostname, unused_port)
+
+        with self.open_file("empty.txt") as fd:
+            fd.write("\n")
+
+        assert self.directory
+        with self.open_file("server.conf") as fd:
+            fd.write(
+                TEMPLATE_CONFIG.format(
+                    hostname=hostname,
+                    port=port,
+                    tls_hostname=tls_hostname,
+                    tls_port=tls_port,
+                    password_field=password_field,
+                    key_path=self.key_path,
+                    pem_path=self.pem_path,
+                    empty_file=os.path.join(self.directory, "empty.txt"),
+                )
+            )
+        self.proc = subprocess.Popen(
+            [
+                "ngircd",
+                "--nodaemon",
+                "--config",
+                os.path.join(self.directory, "server.conf"),
+            ],
+            # stdout=subprocess.DEVNULL,
+        )
+
+        if run_services:
+            self.wait_for_port()
+            self.services_controller = self.services_controller_class(
+                self.test_config, self
+            )
+            self.services_controller.run(
+                protocol="ngircd",
+                server_hostname=hostname,
+                server_port=port,
+            )
+
+
+def get_irctest_controller_class() -> Type[NgircdController]:
+    return NgircdController
diff --git a/irctest/server_tests/bot_mode.py b/irctest/server_tests/bot_mode.py
index 0879391..161aaf4 100644
--- a/irctest/server_tests/bot_mode.py
+++ b/irctest/server_tests/bot_mode.py
@@ -13,7 +13,7 @@ from irctest.patma import ANYDICT, ANYSTR, StrRe
 class BotModeTestCase(cases.BaseServerTestCase):
     def setUp(self):
         super().setUp()
-        self.connectClient("modegetter")
+        self.connectClient("modegettr")
         if "BOT" not in self.server_support:
             raise runner.IsupportTokenNotSupported("BOT")
         self._mode_char = self.server_support["BOT"]
diff --git a/irctest/server_tests/buffering.py b/irctest/server_tests/buffering.py
index 2368915..321e04b 100644
--- a/irctest/server_tests/buffering.py
+++ b/irctest/server_tests/buffering.py
@@ -103,6 +103,9 @@ class BufferingTestCase(cases.BaseServerTestCase):
             if not payload_intact:
                 # truncated
                 self.assertLessEqual(len(received_line), 512, received_line)
+                if received_line.endswith(b"[CUT]\r\n"):
+                    # ngircd
+                    received_line = received_line[0:-7] + b"\r\n"
                 self.assertTrue(
                     payload.encode().startswith(
                         received_line.split(b" ")[-1].strip().lstrip(b":")
diff --git a/irctest/server_tests/kick.py b/irctest/server_tests/kick.py
index bf81ea8..aaafcd5 100644
--- a/irctest/server_tests/kick.py
+++ b/irctest/server_tests/kick.py
@@ -166,6 +166,8 @@ class KickTestCase(cases.BaseServerTestCase):
     @cases.mark_specifications("RFC2812")
     def testKickNonexistentChannel(self):
         """“Kick command [...] Numeric replies: [...] ERR_NOSUCHCHANNEL."""
+        self.connectClient("nick")
+
         self.connectClient("foo")
         self.sendLine(1, "KICK #chan nick")
         m = self.getMessage(1)
diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py
index 572379d..0310903 100644
--- a/irctest/server_tests/list.py
+++ b/irctest/server_tests/list.py
@@ -15,6 +15,9 @@ class ListTestCase(cases.BaseServerTestCase):
         if m.command == "321":
             # skip RPL_LISTSTART
             m = self.getMessage(2)
+        while m.command == "322" and m.params[1] == "&SERVER":
+            # ngircd adds this pseudo-channel
+            m = self.getMessage(2)
         self.assertNotEqual(
             m.command,
             "322",  # RPL_LIST
@@ -55,6 +58,9 @@ class ListTestCase(cases.BaseServerTestCase):
             "nor 323 (RPL_LISTEND) but: {msg}",
         )
         m = self.getMessage(2)
+        while m.command == "322" and m.params[1] == "&SERVER":
+            # ngircd adds this pseudo-channel
+            m = self.getMessage(2)
         self.assertNotEqual(
             m.command,
             "322",  # RPL_LIST
diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py
index 962a3cd..a4807cf 100644
--- a/irctest/server_tests/whois.py
+++ b/irctest/server_tests/whois.py
@@ -161,13 +161,13 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
 class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper):
     @pytest.mark.parametrize(
         "server",
-        ["", "My.Little.Server", "myCoolNick"],
+        ["", "My.Little.Server", "coolNick"],
         ids=["no-target", "target_server", "target-nick"],
     )
     @cases.mark_specifications("RFC2812")
     def testWhoisUser(self, server):
         """Test basic WHOIS behavior"""
-        nick = "myCoolNick"
+        nick = "coolNick"
         username = "myusernam"  # may be truncated if longer than this
         realname = "My User Name"
         self.addClient()
@@ -175,9 +175,9 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.Optionality
         self.sendLine(1, f"USER {username} 0 * :{realname}")
         self.skipToWelcome(1)
 
-        self.connectClient("otherNickname")
+        self.connectClient("otherNick")
         self.getMessages(2)
-        self.sendLine(2, f"WHOIS {server} mycoolnick")
+        self.sendLine(2, f"WHOIS {server} coolnick")
         messages = self.getMessages(2)
         whois_user = messages[0]
         self.assertEqual(whois_user.command, RPL_WHOISUSER)
diff --git a/make_workflows.py b/make_workflows.py
index 0247ff2..f6c0741 100644
--- a/make_workflows.py
+++ b/make_workflows.py
@@ -152,7 +152,11 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
 
         env += test_config.get("env", {}).get(version_flavor.value, "") + " "
         if "prefix" in software_config:
-            env += f"PATH={software_config['prefix']}/bin:$PATH "
+            env += (
+                f"PATH={software_config['prefix']}/sbin"
+                f":{software_config['prefix']}/bin"
+                f":$PATH "
+            )
 
         if software_config["separate_build_job"]:
             needs.append(f"build-{software_id}")
diff --git a/workflows.yml b/workflows.yml
index 9f91b7c..3242cd6 100644
--- a/workflows.yml
+++ b/workflows.yml
@@ -204,6 +204,24 @@ software:
             make -j 4
             make install
 
+    ngircd:
+        name: ngircd
+        repository: ngircd/ngircd
+        refs:
+            stable: rel-26.1
+            release: null
+            devel: master
+            devel_release: null
+        path: ngircd
+        prefix: ~/.local/
+        separate_build_job: true
+        build_script: |
+            cd $GITHUB_WORKSPACE/ngircd
+            ./autogen.sh
+            ./configure --prefix=$HOME/.local/
+            make -j 4
+            make install
+
     snircd:
         name: snircd
         repository: quakenet/snircd
@@ -314,6 +332,15 @@ tests:
     inspircd-anope:
         software: [inspircd, anope]
 
+    ngircd:
+        software: [ngircd]
+
+    ngircd-atheme:
+        software: [ngircd]
+
+    ngircd-anope:
+        software: [ngircd, anope]
+
     plexus4:
         software: [plexus4, anope]