{"record":{"id":"139b95d68a303b1e","repo":"TechnitiumSoftware/DnsServer","slug":"failed-to-add-secondary-node-only-a-primary-node","errorCode":null,"errorMessage":"Failed to add Secondary node: only a Primary node can add a Secondary node to the Cluster.","messagePattern":"Failed to add Secondary node: only a Primary node can add a Secondary node to the Cluster\\.","errorType":"exception","errorClass":"DnsServerException","httpStatus":null,"severity":"error","filePath":"DnsServerCore/Cluster/ClusterManager.cs","lineNumber":689,"sourceCode":"            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to delete Cluster: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to delete Cluster: only a Primary node can delete the Cluster.\");\n\n            if (!forceDelete && (_clusterNodes.Count > 1))\n                throw new DnsServerException(\"Failed to delete Cluster: please remove all Secondary nodes before deleting the Cluster.\");\n\n            DeleteAllClusterConfig();\n        }\n\n        public ClusterNode JoinCluster(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate)\n        {\n            if (!ClusterInitialized)\n                throw new DnsServerException(\"Failed to add Secondary node: the Cluster is not initialized.\");\n\n            if (GetSelfNode().Type != ClusterNodeType.Primary)\n                throw new DnsServerException(\"Failed to add Secondary node: only a Primary node can add a Secondary node to the Cluster.\");\n\n            string secondaryNodeDomain = secondaryNodeUrl.Host.ToLowerInvariant();\n\n            if (!secondaryNodeDomain.EndsWith(\".\" + _clusterDomain, StringComparison.OrdinalIgnoreCase))\n                throw new DnsServerException(\"Failed to add Secondary node: the Secondary node domain name must be a subdomain of the Cluster domain name.\");\n\n            IReadOnlyDictionary<int, ClusterNode> existingClusterNodes = _clusterNodes;\n\n            //validate for duplicate names\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n            {\n                if (existingClusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase))\n                    throw new DnsServerException(\"Failed to add Secondary node: A node with the same DNS Server Domain Name already exists in the Cluster. Please try again after changing the Secondary node's DNS Server Domain Name.\");\n            }\n\n            //add secondary node to cluster nodes\n            ClusterNode secondaryNode = new ClusterNode(this, secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Unknown);\n            Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + 1);","sourceCodeStart":671,"sourceCodeEnd":707,"githubUrl":"https://github.com/TechnitiumSoftware/DnsServer/blob/d0484b6c1e7439cdc53d67d81e9c876cda2ad756/DnsServerCore/Cluster/ClusterManager.cs#L671-L707","documentation":"Thrown by JoinCluster when the current server's self node is not the Primary node. Secondary nodes are added to the cluster by the Primary, which owns the authoritative node list and is responsible for writing NS/A records and catalog zone updates. A Secondary cannot add another Secondary.","triggerScenarios":"JoinCluster is called from a server whose GetSelfNode().Type is not ClusterNodeType.Primary. The guard at line 688 fires after the ClusterInitialized check.","commonSituations":"The add-secondary API request is routed to or executed on a Secondary server rather than the Primary; misconfigured load balancer sending cluster-management traffic to a Secondary backend.","solutions":["Run JoinCluster on the cluster Primary node.","Verify GetSelfNode().Type == ClusterNodeType.Primary before calling."],"exampleFix":"// before\n_dnsWebService.ClusterManager.JoinCluster(nodeId, nodeUrl, nodeIps, cert);\n// after\nif (_dnsWebService.ClusterManager.GetSelfNode().Type != ClusterNodeType.Primary)\n    throw new InvalidOperationException(\"JoinCluster must be called on the Primary node.\");\n_dnsWebService.ClusterManager.JoinCluster(nodeId, nodeUrl, nodeIps, cert);","handlingStrategy":"validation","validationCode":"if (_dnsWebService.ClusterManager.GetSelfNode().Type != ClusterNodeType.Primary)\n    throw new InvalidOperationException(\"JoinCluster must be called on the Primary node.\");\n_dnsWebService.ClusterManager.JoinCluster(nodeId, nodeUrl, nodeIps, cert);","typeGuard":"static bool IsPrimaryNode(ClusterManager cm)\n    => cm.ClusterInitialized && cm.GetSelfNode().Type == ClusterNodeType.Primary;","tryCatchPattern":"try\n{\n    _dnsWebService.ClusterManager.JoinCluster(nodeId, nodeUrl, nodeIps, cert);\n}\ncatch (DnsServerException ex) when (ex.Message.Contains(\"only a Primary node can add\"))\n{\n    throw new InvalidOperationException(\"Redirect this request to the cluster Primary node.\", ex);\n}","preventionTips":["Send all add-secondary requests to the Primary node.","Pin cluster-management API calls to the Primary in load-balanced environments.","Check GetSelfNode().Type before calling JoinCluster."],"tags":["cluster","authorization","primary-node","join","rbac"],"backgroundTag":null,"analyzedSha":"d0484b6c1e7439cdc53d67d81e9c876cda2ad756","analyzedAt":"2026-08-13T22:57:35.508Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}