diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..234ae6b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,84 @@
+name: CI
+
+on:
+  pull_request:
+  push:
+    branches: [master]
+
+jobs:
+  cabal:
+    name: cabal / ghc-${{ matrix.ghc }} / ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os:
+          - ubuntu-latest
+          # - macOS-latest
+        cabal:
+          - "latest"
+        ghc:
+          - "9.2.8"
+          - "9.4.8"
+          - "9.6.3"
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: haskell-actions/setup@v2
+        id: setup-haskell-cabal
+        name: Setup Haskell
+        with:
+          ghc-version: ${{ matrix.ghc }}
+          cabal-version: ${{ matrix.cabal }}
+
+      - uses: actions/cache@v3
+        name: Cache cabal-store
+        with:
+          path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
+          key: ${{ matrix.os }}-${{ matrix.ghc }}-cabal
+
+      - name: Build
+        run: |
+          cabal update
+          cabal build all --enable-tests --enable-benchmarks --write-ghc-environment-files=always
+
+      # TODO: Tests require the `vault` executable to be available.
+      # - name: Test
+      #   run: |
+      #     cabal test all --enable-tests --enable-benchmarks --write-ghc-environment-files=always
+
+  stack:
+    name: stack ${{ matrix.resolver }} / ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os:
+          - ubuntu-latest
+          # - macOS-latest
+        stack: ["latest"]
+        resolver:
+          - "--stack-yaml ./stack.yaml"
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: haskell-actions/setup@v2
+      name: Setup Haskell Stack
+      with:
+        stack-version: ${{ matrix.stack }}
+        enable-stack: true
+
+    - uses: actions/cache@v3
+      name: Cache ~/.stack
+      with:
+        path: ~/.stack
+        key: ${{ matrix.os }}-stack-${{ matrix.resolver }}
+
+    - name: Build
+      run: |
+        stack build --test --bench --no-run-tests --no-run-benchmarks
+
+    # TODO: Tests require the `vault` executable to be available.
+    # - name: Test
+    #   run: |
+    #     stack test --bench --no-run-benchmarks
diff --git a/stack.yaml b/stack.yaml
index 41c8165..0e74d29 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
-resolver: lts-19.6
+resolver: lts-22.30
 flags: {}
 packages:
   - vault-tool
diff --git a/stack.yaml.lock b/stack.yaml.lock
index d8ab114..0aebfde 100644
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -6,7 +6,7 @@
 packages: []
 snapshots:
 - completed:
-    size: 618876
-    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/6.yaml
-    sha256: fb634b19f31da06684bb07ce02a20c75a3162138f279b388905b03ebd57bb50f
-  original: lts-19.6
+    sha256: 795b7a893148a42f09956611a0fa1139293fe6ef934d053468d8e53e3e013390
+    size: 719577
+    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/30.yaml
+  original: lts-22.30
diff --git a/vault-tool/vault-tool.cabal b/vault-tool/vault-tool.cabal
index 2b97e65..fcd1287 100644
--- a/vault-tool/vault-tool.cabal
+++ b/vault-tool/vault-tool.cabal
@@ -33,7 +33,8 @@ library
                        http-client,
                        http-types,
                        http-client-tls,
-                       aeson,
+                       -- vault-tool doesn't yet work with new KeyValue constraint from aeson-2.2
+                       aeson < 2.2,
                        unordered-containers,
                        time