diff --git a/.gitignore b/.gitignore
index a71747c801ac4b08988af303054c5f7b2ea9599e..3a655148c92f0bca3e724fc01904af716685e1db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,6 +95,7 @@
 /tools/kvm-ifup
 /tools/ensure-dirs
 /tools/vcluster-setup
+/tools/node-daemon-setup
 /tools/prepare-node-join
 
 # scripts
diff --git a/Makefile.am b/Makefile.am
index 24f6f22ec5a7db4a0dda9999eaf3b854ecc1f639..67f469fd41774ccd016e4d7620a19608ff23c6d8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -332,6 +332,7 @@ server_PYTHON = \
 pytools_PYTHON = \
 	lib/tools/__init__.py \
 	lib/tools/ensure_dirs.py \
+	lib/tools/node_daemon_setup.py \
 	lib/tools/prepare_node_join.py
 
 utils_PYTHON = \
@@ -616,6 +617,7 @@ PYTHON_BOOTSTRAP_SBIN = \
 PYTHON_BOOTSTRAP = \
 	$(PYTHON_BOOTSTRAP_SBIN) \
 	tools/ensure-dirs \
+	tools/node-daemon-setup \
 	tools/prepare-node-join
 
 qa_scripts = \
@@ -727,6 +729,7 @@ pkglib_python_scripts = \
 
 nodist_pkglib_python_scripts = \
 	tools/ensure-dirs \
+	tools/node-daemon-setup \
 	tools/prepare-node-join
 
 myexeclib_SCRIPTS = \
@@ -970,6 +973,7 @@ python_tests = \
 	test/ganeti.ssh_unittest.py \
 	test/ganeti.storage_unittest.py \
 	test/ganeti.tools.ensure_dirs_unittest.py \
+	test/ganeti.tools.node_daemon_setup_unittest.py \
 	test/ganeti.tools.prepare_node_join_unittest.py \
 	test/ganeti.uidpool_unittest.py \
 	test/ganeti.utils.algo_unittest.py \
@@ -1373,6 +1377,7 @@ daemons/ganeti-%: MODULE = ganeti.server.$(patsubst ganeti-%,%,$(notdir $@))
 daemons/ganeti-watcher: MODULE = ganeti.watcher
 scripts/%: MODULE = ganeti.client.$(subst -,_,$(notdir $@))
 tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs
+tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup
 tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
 $(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst htest/%,%,$@)
 
diff --git a/lib/constants.py b/lib/constants.py
index 180247f1d5422af95ffdddd7d5e0db200ed933cc..ccfa24a78a5104d638f1cca024c1ac9d6b19f7ca 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -2100,5 +2100,11 @@ SSH_DAEMON_KEYFILES = {
   SSHK_DSA: (pathutils.SSH_HOST_DSA_PRIV, pathutils.SSH_HOST_DSA_PUB),
   }
 
+# Node daemon setup
+NDS_CLUSTER_NAME = "cluster_name"
+NDS_NODE_DAEMON_CERTIFICATE = "node_daemon_certificate"
+NDS_SSCONF = "ssconf"
+NDS_START_NODE_DAEMON = "start_node_daemon"
+
 # Do not re-export imported modules
 del re, _vcsversion, _autoconf, socket, pathutils
diff --git a/lib/tools/node_daemon_setup.py b/lib/tools/node_daemon_setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..28bcba0de4d567ec2f899ae0079ce6940b072c6e
--- /dev/null
+++ b/lib/tools/node_daemon_setup.py
@@ -0,0 +1,236 @@
+#
+#
+
+# Copyright (C) 2012 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""Script to configure the node daemon.
+
+"""
+
+import os
+import os.path
+import optparse
+import sys
+import logging
+import OpenSSL
+from cStringIO import StringIO
+
+from ganeti import cli
+from ganeti import constants
+from ganeti import errors
+from ganeti import pathutils
+from ganeti import utils
+from ganeti import serializer
+from ganeti import runtime
+from ganeti import ht
+from ganeti import ssconf
+
+
+_DATA_CHECK = ht.TStrictDict(False, True, {
+  constants.NDS_CLUSTER_NAME: ht.TNonEmptyString,
+  constants.NDS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
+  constants.NDS_SSCONF: ht.TDictOf(ht.TNonEmptyString, ht.TString),
+  constants.NDS_START_NODE_DAEMON: ht.TBool,
+  })
+
+
+class SetupError(errors.GenericError):
+  """Local class for reporting errors.
+
+  """
+
+
+def ParseOptions():
+  """Parses the options passed to the program.
+
+  @return: Options and arguments
+
+  """
+  parser = optparse.OptionParser(usage="%prog [--dry-run]",
+                                 prog=os.path.basename(sys.argv[0]))
+  parser.add_option(cli.DEBUG_OPT)
+  parser.add_option(cli.VERBOSE_OPT)
+  parser.add_option(cli.DRY_RUN_OPT)
+
+  (opts, args) = parser.parse_args()
+
+  return VerifyOptions(parser, opts, args)
+
+
+def VerifyOptions(parser, opts, args):
+  """Verifies options and arguments for correctness.
+
+  """
+  if args:
+    parser.error("No arguments are expected")
+
+  return opts
+
+
+def _VerifyCertificate(cert_pem, _check_fn=utils.CheckNodeCertificate):
+  """Verifies a certificate against the local node daemon certificate.
+
+  @type cert_pem: string
+  @param cert_pem: Certificate and key in PEM format
+  @rtype: string
+  @return: Formatted key and certificate
+
+  """
+  try:
+    cert = \
+      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
+  except Exception, err:
+    raise errors.X509CertError("(stdin)",
+                               "Unable to load certificate: %s" % err)
+
+  try:
+    key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
+  except OpenSSL.crypto.Error, err:
+    raise errors.X509CertError("(stdin)",
+                               "Unable to load private key: %s" % err)
+
+  # Check certificate with given key; this detects cases where the key given on
+  # stdin doesn't match the certificate also given on stdin
+  x509_check_fn = utils.PrepareX509CertKeyCheck(cert, key)
+  try:
+    x509_check_fn()
+  except OpenSSL.SSL.Error:
+    raise errors.X509CertError("(stdin)",
+                               "Certificate is not signed with given key")
+
+  # Standard checks, including check against an existing local certificate
+  # (no-op if that doesn't exist)
+  _check_fn(cert)
+
+  # Format for storing on disk
+  buf = StringIO()
+  buf.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
+  buf.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
+  return buf.getvalue()
+
+
+def VerifyCertificate(data, _verify_fn=_VerifyCertificate):
+  """Verifies cluster certificate.
+
+  @type data: dict
+  @rtype: string
+  @return: Formatted key and certificate
+
+  """
+  cert = data.get(constants.NDS_NODE_DAEMON_CERTIFICATE)
+  if not cert:
+    raise SetupError("Node daemon certificate must be specified")
+
+  return _verify_fn(cert)
+
+
+def VerifyClusterName(data, _verify_fn=ssconf.VerifyClusterName):
+  """Verifies cluster name.
+
+  @type data: dict
+  @rtype: string
+  @return: Cluster name
+
+  """
+  name = data.get(constants.NDS_CLUSTER_NAME)
+  if not name:
+    raise SetupError("Cluster name must be specified")
+
+  _verify_fn(name)
+
+  return name
+
+
+def VerifySsconf(data, cluster_name, _verify_fn=ssconf.VerifyKeys):
+  """Verifies ssconf names.
+
+  @type data: dict
+
+  """
+  items = data.get(constants.NDS_SSCONF)
+
+  if not items:
+    raise SetupError("Ssconf values must be specified")
+
+  # TODO: Should all keys be required? Right now any subset of valid keys is
+  # accepted.
+  _verify_fn(items.keys())
+
+  if items.get(constants.SS_CLUSTER_NAME) != cluster_name:
+    raise SetupError("Cluster name in ssconf does not match")
+
+  return items
+
+
+def LoadData(raw):
+  """Parses and verifies input data.
+
+  @rtype: dict
+
+  """
+  return serializer.LoadAndVerifyJson(raw, _DATA_CHECK)
+
+
+def Main():
+  """Main routine.
+
+  """
+  opts = ParseOptions()
+
+  utils.SetupToolLogging(opts.debug, opts.verbose)
+
+  try:
+    getent = runtime.GetEnts()
+
+    data = LoadData(sys.stdin.read())
+
+    cluster_name = VerifyClusterName(data)
+    cert_pem = VerifyCertificate(data)
+    ssdata = VerifySsconf(data, cluster_name)
+
+    logging.info("Writing ssconf files ...")
+    ssconf.WriteSsconfFiles(ssdata, dry_run=opts.dry_run)
+
+    logging.info("Writing node daemon certificate ...")
+    utils.WriteFile(pathutils.NODED_CERT_FILE, data=cert_pem,
+                    mode=pathutils.NODED_CERT_MODE,
+                    uid=getent.masterd_uid, gid=getent.masterd_gid,
+                    dry_run=opts.dry_run)
+
+    if (data.get(constants.NDS_START_NODE_DAEMON) and # pylint: disable=E1103
+        not opts.dry_run):
+      logging.info("Restarting node daemon ...")
+
+      cmd = ("%s stop-all; %s start %s" %
+             (pathutils.DAEMON_UTIL, pathutils.DAEMON_UTIL, constants.NODED))
+
+      result = utils.RunCmd(cmd, interactive=True)
+      if result.failed:
+        raise SetupError("Could not start the node daemon, command '%s'"
+                         " failed: %s" % (result.cmd, result.fail_reason))
+
+    logging.info("Node daemon successfully configured")
+  except Exception, err: # pylint: disable=W0703
+    logging.debug("Caught unhandled exception", exc_info=True)
+
+    (retcode, message) = cli.FormatError(err)
+    logging.error(message)
+
+    return retcode
+  else:
+    return constants.EXIT_SUCCESS
diff --git a/test/ganeti.tools.node_daemon_setup_unittest.py b/test/ganeti.tools.node_daemon_setup_unittest.py
new file mode 100755
index 0000000000000000000000000000000000000000..dc05fe045c320fd63a5a3bc046deefd1c333f5a1
--- /dev/null
+++ b/test/ganeti.tools.node_daemon_setup_unittest.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2012 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""Script for testing ganeti.tools.node_daemon_setup"""
+
+import unittest
+import shutil
+import tempfile
+import os.path
+import OpenSSL
+
+from ganeti import errors
+from ganeti import constants
+from ganeti import serializer
+from ganeti import pathutils
+from ganeti import compat
+from ganeti import utils
+from ganeti.tools import node_daemon_setup
+
+import testutils
+
+
+_SetupError = node_daemon_setup.SetupError
+
+
+class TestLoadData(unittest.TestCase):
+  def testNoJson(self):
+    for data in ["", "{", "}"]:
+      self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, data)
+
+  def testInvalidDataStructure(self):
+    raw = serializer.DumpJson({
+      "some other thing": False,
+      })
+    self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, raw)
+
+    raw = serializer.DumpJson([])
+    self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, raw)
+
+  def testValidData(self):
+    raw = serializer.DumpJson({})
+    self.assertEqual(node_daemon_setup.LoadData(raw), {})
+
+
+class TestVerifyCertificate(testutils.GanetiTestCase):
+  def setUp(self):
+    testutils.GanetiTestCase.setUp(self)
+    self.tmpdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    testutils.GanetiTestCase.tearDown(self)
+    shutil.rmtree(self.tmpdir)
+
+  def testNoCert(self):
+    self.assertRaises(_SetupError, node_daemon_setup.VerifyCertificate,
+                      {}, _verify_fn=NotImplemented)
+
+  def testVerificationSuccessWithCert(self):
+    node_daemon_setup.VerifyCertificate({
+      constants.NDS_NODE_DAEMON_CERTIFICATE: "something",
+      }, _verify_fn=lambda _: None)
+
+  def testNoPrivateKey(self):
+    cert_filename = self._TestDataFilename("cert1.pem")
+    cert_pem = utils.ReadFile(cert_filename)
+
+    self.assertRaises(errors.X509CertError,
+                      node_daemon_setup._VerifyCertificate,
+                      cert_pem, _check_fn=NotImplemented)
+
+  def testInvalidCertificate(self):
+    self.assertRaises(errors.X509CertError,
+                      node_daemon_setup._VerifyCertificate,
+                      "Something that's not a certificate",
+                      _check_fn=NotImplemented)
+
+  @staticmethod
+  def _Check(cert):
+    assert cert.get_subject()
+
+  def testSuccessfulCheck(self):
+    cert_filename = self._TestDataFilename("cert2.pem")
+    cert_pem = utils.ReadFile(cert_filename)
+    result = \
+      node_daemon_setup._VerifyCertificate(cert_pem, _check_fn=self._Check)
+    self.assertTrue("-----BEGIN PRIVATE KEY-----" in result)
+    self.assertTrue("-----BEGIN CERTIFICATE-----" in result)
+
+  def testMismatchingKey(self):
+    cert1_path = self._TestDataFilename("cert1.pem")
+    cert2_path = self._TestDataFilename("cert2.pem")
+
+    # Extract certificate
+    cert1 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+                                            utils.ReadFile(cert1_path))
+    cert1_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
+                                                cert1)
+
+    # Extract mismatching key
+    key2 = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
+                                          utils.ReadFile(cert2_path))
+    key2_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
+                                              key2)
+
+    try:
+      node_daemon_setup._VerifyCertificate(cert1_pem + key2_pem,
+                                           _check_fn=NotImplemented)
+    except errors.X509CertError, err:
+      self.assertEqual(err.args,
+                       ("(stdin)", "Certificate is not signed with given key"))
+    else:
+      self.fail("Exception was not raised")
+
+
+class TestVerifyClusterName(unittest.TestCase):
+  def setUp(self):
+    unittest.TestCase.setUp(self)
+    self.tmpdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    unittest.TestCase.tearDown(self)
+    shutil.rmtree(self.tmpdir)
+
+  def testNoName(self):
+    self.assertRaises(_SetupError, node_daemon_setup.VerifyClusterName,
+                      {}, _verify_fn=NotImplemented)
+
+  @staticmethod
+  def _FailingVerify(name):
+    assert name == "somecluster.example.com"
+    raise errors.GenericError()
+
+  def testFailingVerification(self):
+    data = {
+      constants.NDS_CLUSTER_NAME: "somecluster.example.com",
+      }
+
+    self.assertRaises(errors.GenericError, node_daemon_setup.VerifyClusterName,
+                      data, _verify_fn=self._FailingVerify)
+
+  def testSuccess(self):
+    data = {
+      constants.NDS_CLUSTER_NAME: "cluster.example.com",
+      }
+
+    result = \
+      node_daemon_setup.VerifyClusterName(data, _verify_fn=lambda _: None)
+
+    self.assertEqual(result, "cluster.example.com")
+
+
+class TestVerifySsconf(unittest.TestCase):
+  def testNoSsconf(self):
+    self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf,
+                      {}, NotImplemented, _verify_fn=NotImplemented)
+
+    for items in [None, {}]:
+      self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf, {
+        constants.NDS_SSCONF: items,
+        }, NotImplemented, _verify_fn=NotImplemented)
+
+  def _Check(self, names):
+    self.assertEqual(frozenset(names), frozenset([
+      constants.SS_CLUSTER_NAME,
+      constants.SS_INSTANCE_LIST,
+      ]))
+
+  def testSuccess(self):
+    ssdata = {
+      constants.SS_CLUSTER_NAME: "cluster.example.com",
+      constants.SS_INSTANCE_LIST: [],
+      }
+
+    result = node_daemon_setup.VerifySsconf({
+      constants.NDS_SSCONF: ssdata,
+      }, "cluster.example.com", _verify_fn=self._Check)
+
+    self.assertEqual(result, ssdata)
+
+    self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf, {
+      constants.NDS_SSCONF: ssdata,
+      }, "wrong.example.com", _verify_fn=self._Check)
+
+  def testInvalidKey(self):
+    self.assertRaises(errors.GenericError, node_daemon_setup.VerifySsconf, {
+      constants.NDS_SSCONF: {
+        "no-valid-ssconf-key": "value",
+        },
+      }, NotImplemented)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()