feat(fw): Migration 0010 — Firewall-v2-Schema (Fortigate-Style)
Phase-1 firewall_rules (chain/match_expr raw nft) → Fortigate-Niveau: * firewall_address_objects (host/network/range/fqdn) * firewall_address_groups + members junction * firewall_services (proto+port range, builtin-Flag) * firewall_service_groups + members junction * firewall_rules komplett umgebaut: src_zone+addr/group/cidr, dst_zone+addr/group/cidr, service_object_id ODER service_group_id, action accept|drop|reject, log-Flag, priority+enabled * firewall_nat_rules (kind=dnat|snat|masquerade) als separate Tabelle Zonen kommen aus network_interfaces.role (wan|lan|dmz|mgmt|cluster + pseudo-Zone 'any'). Builtin-Inserts: 18 Standard-Services (HTTP/HTTPS/SSH/DNS/SMTP-Familie/ DBs/RDP/WireGuard/Ping) plus 5 Service-Groups (Web, Mail-Submit, Mail-Receive, DNS, Ping). Renderer (internal/firewall/firewall.go) lässt firewall_rules-Query für jetzt aus — Template fällt auf baseline + cluster-peer-set zurück. Volle Render-Logik mit den neuen Joins kommt mit Task #44. Models + Repos + Handlers + Frontend folgen in den nächsten Commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
249
internal/database/migrations/0010_firewall_v2.sql
Normal file
249
internal/database/migrations/0010_firewall_v2.sql
Normal file
@@ -0,0 +1,249 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════
|
||||
-- Firewall v2 — Fortigate-style:
|
||||
-- address objects + groups · service objects + groups · zone-aware
|
||||
-- policy rules · separate NAT rules.
|
||||
--
|
||||
-- Zonen sind nicht als Tabelle modelliert — sie ergeben sich aus
|
||||
-- network_interfaces.role (wan|lan|dmz|mgmt|cluster), plus die
|
||||
-- pseudo-Zone 'any' im Application-Layer.
|
||||
--
|
||||
-- Die alte firewall_rules-Tabelle aus Phase 1 bekommt einen neuen
|
||||
-- Aufbau. Bestehende Rows (es gibt im Moment keine in produktion)
|
||||
-- werden gedroppt — TRUNCATE ist OK.
|
||||
-- ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DROP TABLE IF EXISTS firewall_rules;
|
||||
|
||||
-- ── Address Objects ────────────────────────────────────────────────────
|
||||
-- Wiederverwendbare Adress-Definitionen. kind=host (eine IP),
|
||||
-- network (CIDR), range (a-b), fqdn (DNS-Name; wird zur render-time
|
||||
-- aufgelöst und als Set materialisiert).
|
||||
CREATE TABLE IF NOT EXISTS firewall_address_objects (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT firewall_address_objects_name_unique UNIQUE (name),
|
||||
CONSTRAINT firewall_address_objects_kind_check CHECK (kind IN ('host', 'network', 'range', 'fqdn'))
|
||||
);
|
||||
|
||||
-- ── Address Groups ─────────────────────────────────────────────────────
|
||||
-- Gruppen referenzieren primitive Objekte (kein Nesting in v1 — wir
|
||||
-- können Group-in-Group später flach via DAG erlauben).
|
||||
CREATE TABLE IF NOT EXISTS firewall_address_groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT firewall_address_groups_name_unique UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS firewall_address_group_members (
|
||||
group_id BIGINT NOT NULL REFERENCES firewall_address_groups(id) ON DELETE CASCADE,
|
||||
object_id BIGINT NOT NULL REFERENCES firewall_address_objects(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (group_id, object_id)
|
||||
);
|
||||
|
||||
-- ── Services ───────────────────────────────────────────────────────────
|
||||
-- Protocol + Port Range. proto='any' = alle (kein Port-Match).
|
||||
-- ICMP wird via proto='icmp'/'icmpv6' modelliert; port_start/end NULL.
|
||||
CREATE TABLE IF NOT EXISTS firewall_services (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
proto TEXT NOT NULL,
|
||||
port_start INTEGER,
|
||||
port_end INTEGER,
|
||||
builtin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT firewall_services_name_unique UNIQUE (name),
|
||||
CONSTRAINT firewall_services_proto_check CHECK (proto IN ('tcp', 'udp', 'icmp', 'icmpv6', 'any')),
|
||||
CONSTRAINT firewall_services_port_check CHECK (
|
||||
(proto IN ('tcp', 'udp') AND port_start IS NOT NULL AND port_end IS NOT NULL
|
||||
AND port_start BETWEEN 1 AND 65535 AND port_end BETWEEN port_start AND 65535)
|
||||
OR (proto NOT IN ('tcp', 'udp'))
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS firewall_service_groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT firewall_service_groups_name_unique UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS firewall_service_group_members (
|
||||
group_id BIGINT NOT NULL REFERENCES firewall_service_groups(id) ON DELETE CASCADE,
|
||||
service_id BIGINT NOT NULL REFERENCES firewall_services(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (group_id, service_id)
|
||||
);
|
||||
|
||||
-- ── Policy Rules ───────────────────────────────────────────────────────
|
||||
-- Quelle und Ziel können je sein:
|
||||
-- * src_address_object_id (FK) — referenziert ein primitives Objekt
|
||||
-- * src_address_group_id (FK) — referenziert eine Gruppe
|
||||
-- * src_cidr (TEXT) — Inline-Fallback für ad-hoc-Regeln
|
||||
-- * (alle null = 'any', d.h. „beliebig")
|
||||
--
|
||||
-- Genau eine der drei darf gesetzt sein (Anwendungs-Validation in der
|
||||
-- Handler-Schicht; DB lässt die Kombi zu, weil expressive
|
||||
-- CHECK-Constraints schwer zu warten sind).
|
||||
--
|
||||
-- Service analog: object oder group oder „any".
|
||||
CREATE TABLE IF NOT EXISTS firewall_rules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
action TEXT NOT NULL,
|
||||
|
||||
src_zone TEXT NOT NULL DEFAULT 'any',
|
||||
src_address_object_id BIGINT REFERENCES firewall_address_objects(id) ON DELETE SET NULL,
|
||||
src_address_group_id BIGINT REFERENCES firewall_address_groups(id) ON DELETE SET NULL,
|
||||
src_cidr TEXT,
|
||||
|
||||
dst_zone TEXT NOT NULL DEFAULT 'any',
|
||||
dst_address_object_id BIGINT REFERENCES firewall_address_objects(id) ON DELETE SET NULL,
|
||||
dst_address_group_id BIGINT REFERENCES firewall_address_groups(id) ON DELETE SET NULL,
|
||||
dst_cidr TEXT,
|
||||
|
||||
service_object_id BIGINT REFERENCES firewall_services(id) ON DELETE SET NULL,
|
||||
service_group_id BIGINT REFERENCES firewall_service_groups(id) ON DELETE SET NULL,
|
||||
|
||||
log BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
comment TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT firewall_rules_action_check CHECK (action IN ('accept', 'drop', 'reject')),
|
||||
CONSTRAINT firewall_rules_src_zone_check CHECK (src_zone IN ('wan', 'lan', 'dmz', 'mgmt', 'cluster', 'any')),
|
||||
CONSTRAINT firewall_rules_dst_zone_check CHECK (dst_zone IN ('wan', 'lan', 'dmz', 'mgmt', 'cluster', 'any'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_firewall_rules_priority ON firewall_rules (priority DESC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS idx_firewall_rules_enabled ON firewall_rules (enabled) WHERE enabled;
|
||||
|
||||
-- ── NAT Rules (separate Tabelle) ───────────────────────────────────────
|
||||
-- kind:
|
||||
-- dnat — Destination NAT (port-forward von außen nach intern)
|
||||
-- snat — Source NAT (rewrite source-IP zu fester Adresse)
|
||||
-- masquerade — Source NAT zur Adresse des Out-Interfaces
|
||||
-- (typisch lan→wan ohne fester ip)
|
||||
--
|
||||
-- Felder pro kind:
|
||||
-- dnat: in_zone + match_dst_port → target_addr [+ target_port]
|
||||
-- snat: out_zone + match_src_cidr → target_addr
|
||||
-- masquerade: out_zone + match_src_cidr (kein target — Iface-IP)
|
||||
CREATE TABLE IF NOT EXISTS firewall_nat_rules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
kind TEXT NOT NULL,
|
||||
|
||||
in_zone TEXT,
|
||||
out_zone TEXT,
|
||||
proto TEXT,
|
||||
match_src_cidr TEXT,
|
||||
match_dst_cidr TEXT,
|
||||
match_dport_start INTEGER,
|
||||
match_dport_end INTEGER,
|
||||
|
||||
target_addr TEXT,
|
||||
target_port_start INTEGER,
|
||||
target_port_end INTEGER,
|
||||
|
||||
comment TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT firewall_nat_rules_kind_check CHECK (kind IN ('dnat', 'snat', 'masquerade')),
|
||||
CONSTRAINT firewall_nat_rules_proto_check CHECK (proto IS NULL OR proto IN ('tcp', 'udp', 'any')),
|
||||
CONSTRAINT firewall_nat_rules_zone_check CHECK (
|
||||
(in_zone IS NULL OR in_zone IN ('wan', 'lan', 'dmz', 'mgmt', 'cluster')) AND
|
||||
(out_zone IS NULL OR out_zone IN ('wan', 'lan', 'dmz', 'mgmt', 'cluster'))
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_firewall_nat_rules_priority ON firewall_nat_rules (priority DESC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS idx_firewall_nat_rules_enabled ON firewall_nat_rules (enabled) WHERE enabled;
|
||||
|
||||
-- ── Builtin-Services ───────────────────────────────────────────────────
|
||||
INSERT INTO firewall_services (name, proto, port_start, port_end, builtin, description) VALUES
|
||||
('HTTP', 'tcp', 80, 80, TRUE, 'Hypertext Transfer Protocol'),
|
||||
('HTTPS', 'tcp', 443, 443, TRUE, 'HTTP over TLS'),
|
||||
('SSH', 'tcp', 22, 22, TRUE, 'Secure Shell'),
|
||||
('DNS-TCP', 'tcp', 53, 53, TRUE, 'DNS over TCP (zone transfer / large responses)'),
|
||||
('DNS-UDP', 'udp', 53, 53, TRUE, 'DNS over UDP (standard query)'),
|
||||
('NTP', 'udp', 123, 123, TRUE, 'Network Time Protocol'),
|
||||
('SMTP', 'tcp', 25, 25, TRUE, 'Simple Mail Transfer Protocol'),
|
||||
('SMTPS', 'tcp', 465, 465, TRUE, 'SMTP over implicit TLS'),
|
||||
('Submission','tcp', 587, 587, TRUE, 'SMTP message submission'),
|
||||
('IMAPS', 'tcp', 993, 993, TRUE, 'IMAP over implicit TLS'),
|
||||
('POP3S', 'tcp', 995, 995, TRUE, 'POP3 over implicit TLS'),
|
||||
('MySQL', 'tcp', 3306, 3306, TRUE, 'MySQL / MariaDB'),
|
||||
('PostgreSQL','tcp', 5432, 5432, TRUE, 'PostgreSQL'),
|
||||
('Redis', 'tcp', 6379, 6379, TRUE, 'Redis / KeyDB'),
|
||||
('RDP', 'tcp', 3389, 3389, TRUE, 'Microsoft Remote Desktop'),
|
||||
('WireGuard', 'udp', 51820, 51820, TRUE, 'WireGuard VPN (default port)'),
|
||||
('PING-v4', 'icmp', NULL, NULL, TRUE, 'ICMP echo (ping)'),
|
||||
('PING-v6', 'icmpv6', NULL, NULL, TRUE, 'ICMPv6 echo (ping6)')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ── Builtin Service-Groups ─────────────────────────────────────────────
|
||||
INSERT INTO firewall_service_groups (name, description) VALUES
|
||||
('Web', 'HTTP + HTTPS'),
|
||||
('Mail-Submit', 'Outgoing mail submission'),
|
||||
('Mail-Receive', 'Incoming mail (SMTP family)'),
|
||||
('DNS', 'DNS over TCP and UDP'),
|
||||
('Ping', 'ICMP echo for IPv4 + IPv6')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Members for the builtin groups. Joins via subquery so we don't
|
||||
-- depend on auto-incremented IDs in the inserts above.
|
||||
INSERT INTO firewall_service_group_members (group_id, service_id)
|
||||
SELECT g.id, s.id FROM firewall_service_groups g, firewall_services s
|
||||
WHERE (g.name = 'Web' AND s.name IN ('HTTP', 'HTTPS'))
|
||||
OR (g.name = 'Mail-Submit' AND s.name IN ('SMTPS', 'Submission'))
|
||||
OR (g.name = 'Mail-Receive' AND s.name IN ('SMTP', 'SMTPS', 'Submission'))
|
||||
OR (g.name = 'DNS' AND s.name IN ('DNS-TCP', 'DNS-UDP'))
|
||||
OR (g.name = 'Ping' AND s.name IN ('PING-v4', 'PING-v6'))
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS firewall_nat_rules;
|
||||
DROP TABLE IF EXISTS firewall_rules;
|
||||
DROP TABLE IF EXISTS firewall_service_group_members;
|
||||
DROP TABLE IF EXISTS firewall_service_groups;
|
||||
DROP TABLE IF EXISTS firewall_services;
|
||||
DROP TABLE IF EXISTS firewall_address_group_members;
|
||||
DROP TABLE IF EXISTS firewall_address_groups;
|
||||
DROP TABLE IF EXISTS firewall_address_objects;
|
||||
|
||||
-- Restore the Phase-1 firewall_rules shape so a `migrate down`
|
||||
-- doesn't leave the DB without that table.
|
||||
CREATE TABLE IF NOT EXISTS firewall_rules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
chain TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
match_expr TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
comment TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
Reference in New Issue
Block a user