-- +goose Up -- +goose StatementBegin -- firewall_zones promotes zones from a hard-coded enum -- (wan/lan/dmz/mgmt/cluster) into a first-class entity. Operators -- can add their own (e.g. iot, guest, voip) without a schema -- change. Existing role/zone TEXT columns on network_interfaces, -- firewall_rules and firewall_nat_rules continue to store the -- zone NAME — referential integrity is enforced at the application -- layer (handler validates name exists in firewall_zones), not by -- a hard FK, so 'any' on rules and NULL on NAT keep working -- without special-casing. -- -- builtin = TRUE marks the seed zones; the API rejects DELETE on -- those rows to prevent the operator from removing a zone the -- renderer still expects. CREATE TABLE IF NOT EXISTS firewall_zones ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT, builtin BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT firewall_zones_name_check CHECK (name ~ '^[a-z][a-z0-9_-]{0,31}$') ); INSERT INTO firewall_zones (name, description, builtin) VALUES ('wan', 'Public-facing internet uplink', TRUE), ('lan', 'Internal trusted network', TRUE), ('dmz', 'Quarantined service network', TRUE), ('mgmt', 'Admin-only management network', TRUE), ('cluster', 'Inter-node cluster traffic (KeyDB / mTLS API)', TRUE) ON CONFLICT (name) DO NOTHING; -- Drop the hard-coded CHECK constraints so the operator can declare -- new zones without the SQL layer rejecting them. App-side -- validation in the handlers takes over. ALTER TABLE network_interfaces DROP CONSTRAINT IF EXISTS network_interfaces_role_check; ALTER TABLE firewall_rules DROP CONSTRAINT IF EXISTS firewall_rules_src_zone_check; ALTER TABLE firewall_rules DROP CONSTRAINT IF EXISTS firewall_rules_dst_zone_check; ALTER TABLE firewall_nat_rules DROP CONSTRAINT IF EXISTS firewall_nat_rules_zone_check; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP TABLE IF EXISTS firewall_zones; -- +goose StatementEnd