diff --git a/htools/Ganeti/HTools/QC.hs b/htools/Ganeti/HTools/QC.hs
index 15e591c8e8e1803db0a39729e0964a441bbc30ba..37736806eb8d22c2bed5d6352b3f5d9b5d95c9ca 100644
--- a/htools/Ganeti/HTools/QC.hs
+++ b/htools/Ganeti/HTools/QC.hs
@@ -50,6 +50,7 @@ module Ganeti.HTools.QC
   , testSsconf
   , testRpc
   , testQlang
+  , testConfd
   ) where
 
 import qualified Test.HUnit as HUnit
@@ -2089,3 +2090,74 @@ prop_Qlang_Serialisation =
 testSuite "Qlang"
   [ 'prop_Qlang_Serialisation
   ]
+
+
+-- * Confd tests (generic library)
+
+instance Arbitrary Confd.ConfdRequestType where
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Confd.ConfdReqField where
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Confd.ConfdReqQ where
+  arbitrary = Confd.ConfdReqQ <$> arbitrary <*> arbitrary <*>
+              arbitrary <*> arbitrary
+
+instance Arbitrary Confd.ConfdQuery where
+  arbitrary = oneof [ pure Confd.EmptyQuery
+                    , Confd.PlainQuery <$> getName
+                    , Confd.DictQuery <$> arbitrary
+                    ]
+
+instance Arbitrary Confd.ConfdRequest where
+  arbitrary = Confd.ConfdRequest <$> arbitrary <*> arbitrary <*> arbitrary
+              <*> arbitrary
+
+-- | Test that signing messages and checking signatures is correct. It
+-- also tests, indirectly the serialisation of messages so we don't
+-- need a separate test for that.
+prop_Confd_req_sign :: Hash.HashKey        -- ^ The hash key
+                    -> NonNegative Integer -- ^ The base timestamp
+                    -> Positive Integer    -- ^ Delta for out of window
+                    -> Bool                -- ^ Whether delta should be + or -
+                    -> Confd.ConfdRequest
+                    -> Property
+prop_Confd_req_sign key (NonNegative timestamp) (Positive bad_delta) pm crq =
+  forAll (choose (0, fromIntegral C.confdMaxClockSkew)) $ \ good_delta ->
+  let encoded = J.encode crq
+      salt = show timestamp
+      signed = J.encode $ Confd.Utils.signMessage key salt encoded
+      good_timestamp = timestamp + if pm then good_delta else (-good_delta)
+      bad_delta' = fromIntegral C.confdMaxClockSkew + bad_delta
+      bad_timestamp = timestamp + if pm then bad_delta' else (-bad_delta')
+      ts_ok = Confd.Utils.parseMessage key signed good_timestamp
+      ts_bad = Confd.Utils.parseMessage key signed bad_timestamp
+  in printTestCase "Failed to parse good message"
+       (ts_ok ==? Types.Ok (encoded, crq)) .&&.
+     printTestCase ("Managed to deserialise message with bad\
+                    \ timestamp, got " ++ show ts_bad)
+       (ts_bad ==? Types.Bad "Too old/too new timestamp or clock skew")
+
+-- | Tests that signing with a different key fails detects failure
+-- correctly.
+prop_Confd_bad_key :: String             -- ^ Salt
+                   -> Confd.ConfdRequest -- ^ Request
+                   -> Property
+prop_Confd_bad_key salt crq =
+  -- fixme: we hardcode here the expected length of a sha1 key, as
+  -- otherwise we could have two short keys that differ only in the
+  -- final zero elements count, and those will be expanded to be the
+  -- same
+  forAll (vector 20) $ \key_sign ->
+  forAll (vector 20 `suchThat` (/= key_sign)) $ \key_verify ->
+  let signed = Confd.Utils.signMessage key_sign salt (J.encode crq)
+      encoded = J.encode signed
+  in printTestCase ("Accepted message signed with different key" ++ encoded) $
+    Types.Bad "HMAC verification failed" ==?
+     Confd.Utils.parseRequest key_verify encoded
+
+testSuite "Confd"
+  [ 'prop_Confd_req_sign
+  , 'prop_Confd_bad_key
+  ]
diff --git a/htools/test.hs b/htools/test.hs
index ca2fc7e4b39e85110fbed9ed411aa2de233015e5..7f3da8b9f2b6f2653174e64c3b46c066690a1efc 100644
--- a/htools/test.hs
+++ b/htools/test.hs
@@ -70,6 +70,7 @@ allTests =
   , (True, testSsconf)
   , (True, testQlang)
   , (True, testRpc)
+  , (True, testConfd)
   , (False, testCluster)
   ]