{"record":{"id":"308461656c45d125","repo":"slackhq/nebula","slug":"s-failed-to-parse-should-be-an-array-of-rules","errorCode":null,"errorMessage":"%s failed to parse, should be an array of rules","messagePattern":"(.+?) failed to parse, should be an array of rules","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"firewall.go","lineNumber":335,"sourceCode":"\treturn \"SHA:\" + f.GetRuleHash() + \",FNV:\" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)\n}\n\nfunc AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw FirewallInterface) error {\n\tvar table string\n\tif inbound {\n\t\ttable = \"firewall.inbound\"\n\t} else {\n\t\ttable = \"firewall.outbound\"\n\t}\n\n\tr := c.Get(table)\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\trs, ok := r.([]any)\n\tif !ok {\n\t\treturn fmt.Errorf(\"%s failed to parse, should be an array of rules\", table)\n\t}\n\n\tfor i, t := range rs {\n\t\tr, err := convertRule(l, t, table, i)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s rule #%v; %s\", table, i, err)\n\t\t}\n\n\t\tif r.Code != \"\" && r.Port != \"\" {\n\t\t\treturn fmt.Errorf(\"%s rule #%v; only one of port or code should be provided\", table, i)\n\t\t}\n\n\t\tif r.Host == \"\" && len(r.Groups) == 0 && r.Cidr == \"\" && r.LocalCidr == \"\" && r.CAName == \"\" && r.CASha == \"\" {\n\t\t\treturn fmt.Errorf(\"%s rule #%v; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided\", table, i)\n\t\t}\n\n\t\tvar sPort, errPort string\n\t\tif r.Code != \"\" {","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/firewall.go#L317-L353","documentation":"AddFirewallRulesFromConfig expects the YAML value for a firewall table (inbound/outbound) to be an array of rule maps. If the decoded value is present but not a []any (e.g. it's a map or scalar), this error is returned naming the offending table.","triggerScenarios":"Calling AddFirewallRulesFromConfig (directly or via NewFirewallFromConfig) where the config's \"firewall.inbound\" or \"firewall.outbound\" is defined but is not a YAML list — e.g. \"inbound: {}\" or \"inbound: true\".","commonSituations":"YAML indentation mistakes that turn the list of rules into a nested map, copy-pasting a single rule object instead of a one-element list, or wrong key nesting under the firewall stanza.","solutions":["Make the table value a YAML array of rule maps, even for a single rule.","Check indentation: each rule must be a list item (leading dash) under inbound/outbound.","If the table should be empty, remove the key entirely rather than setting it to a non-list value."],"exampleFix":"// before (config)\nfirewall:\n  inbound:\n    port: 443\n    proto: tcp\n// after\nfirewall:\n  inbound:\n    - port: 443\n      proto: tcp","handlingStrategy":"validation","validationCode":"func validateFirewallTable(cfg map[string]any, table string) error {\n    v, ok := cfg[\"firewall\"].(map[string]any)\n    if !ok { return nil }\n    t, present := v[table]\n    if !present || t == nil { return nil }\n    if _, ok := t.([]any); !ok {\n        return fmt.Errorf(\"firewall.%s must be a list of rules\", table)\n    }\n    return nil\n}","typeGuard":"func isRuleList(v any) bool {\n    _, ok := v.([]any)\n    return ok\n}","tryCatchPattern":"err := fw.AddFirewallRulesFromConfig(l, \"inbound\", rawInbound)\nif err != nil {\n    return fmt.Errorf(\"invalid firewall config: %w\", err)\n}","preventionTips":["Always use YAML list syntax (dashes) for inbound/outbound rules.","Run the config through nebula's config test/parse before deploying.","Validate YAML structure with a schema (JSON Schema for nebula config) in CI."],"tags":["firewall","yaml","configuration","validation"],"backgroundTag":"invalid-config-shape","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}