frostmoln_security_group_rules (Data Source)
Lists every rule stored on one security group — the rows the rules panel and fm would show, rendered exactly as frostmoln_security_group_rule writes them.
This is the drift detector the per-rule resource cannot be. frostmoln_security_group_rule is one resource per rule, and the parent frostmoln_security_group manages the group and nothing else, so a rule added out of band (in the portal, with fm, through the API, or by a colleague) is invisible to every plan: no resource holds its id, nothing looks for it, and it appears in no plan and is removed by no apply. Rules are not even directly GETtable — the group read itself is the only list — so this data source reads the group's WHOLE stored set and renders it verbatim, and a check block or lifecycle postcondition over the listing turns an out-of-band addition into a failed plan (see the example and the surface-contract guide). Rules are additive — traffic matching ANY rule is allowed — so every unplanned row makes the group more permissive than the configuration says, and one world-open rule is as dangerous as the set of them.
WHAT A NULL REMOTE MEANS: a row whose remote_cidr and remote_group_id are BOTH null constrains the source not at all. On EGRESS that row is normally one of the two platform-injected allow-all egress rules every group carries until deleted — unmanaged by Terraform, benign, and documented in the frostmoln_security_group_rule description. On INGRESS it means the rule allows traffic from ANY source. The platform refuses to create a rule with no remote today, so an ingress row with both remotes null predates that refusal and is a world-open hole to close, not a cosmetic curiosity. The listing renders what is stored, verbatim — surfacing these rows is this data source's job, never hiding them.
GROUPS THE PLATFORM PROVISIONED FOR MANAGED SERVICES READ FINE AND CAN NEVER BE MANAGED. The parent groups for a managed database, cache, webserver, messaging instance, Kubernetes cluster or Application Gateway are visible in your account, so this listing happily shows their rules — but every rule write on one is refused with 409 / resource_in_use, permanently. Read them to know what the platform enforces; point frostmoln_security_group_rule resources and imports only at groups you created.
IMPORT RECIPE: bring an out-of-band rule under management with terraform import frostmoln_security_group_rule.<name> <security_group_id>/<rule_id>. One exception — the injected allow-all egress pair: import it ONLY IN ORDER TO DESTROY IT, and remove the block from configuration once the destroy has run. Leaving the block re-creates a rule the platform refuses (no rule may be created with an empty remote), so the apply would fail forever after; frostmoln_security_group's delete_default_egress and the resource description carry the full contract.
Example Usage
# The drift-detection recipe for security group rules.
#
# `frostmoln_security_group_rule` is one resource per rule and the parent
# `frostmoln_security_group` manages the group and nothing else, so a rule added
# out of band (portal, `fm`, API, a colleague) is invisible to every plan. Rules
# are additive — traffic matching ANY rule is allowed — so every unplanned row
# makes the group more permissive than the configuration says; ONE rule open to
# the world is as dangerous as several. This data source reads the group's
# whole stored set (the group read embeds its rules; rules have no GET of their
# own), and the check block below pins it by id — rules carry server ids, so
# the pin is id-based, not signature-based.
data "frostmoln_security_group_rules" "app" {
security_group_id = frostmoln_security_group.app.id
}
# Rule ids this configuration manages. One rule here; with several, give the
# resource a for_each and use ids = toset(values(...)[*].id).
locals {
managed_rule_ids = [
frostmoln_security_group_rule.ssh_ingress.id,
]
}
# EVERY row on the group must be a rule this configuration declares — except
# the platform-injected allow-all egress pair, whose exact signature is
# egress + any + BOTH remotes null. The filter bets on a platform invariant:
# the API refuses to CREATE a rule with an empty remote, so nothing else can
# ever match that signature — if that refusal ever loosens, tighten the filter.
# An INGRESS row with both remotes null is the opposite of benign — it allows
# traffic from ANY source (fail-open) — and MUST NOT be filtered: the check
# below rightly fails on one until it is deleted. On new groups the cleaner
# answer is delete_default_egress = true on the frostmoln_security_group
# resource, which removes the pair at create.
check "security_group_rules_pinned" {
data = data.frostmoln_security_group_rules.app
assert {
condition = length([
for row in data.frostmoln_security_group_rules.app.rules : row.id
if !contains(local.managed_rule_ids, row.id) && !(
row.direction == "egress" &&
row.protocol == "any" &&
row.remote_cidr == null &&
row.remote_group_id == null
)
]) == 0
error_message = (
"This security group has rules Terraform does not manage. They were added outside Terraform — " +
"in the portal, with `fm`, or by someone else — and rules are additive, so the group now allows " +
"more than the configuration says. If any row has ingress and neither remote set, it allows " +
"traffic from ANY source; close it first. Import the rule with " +
"`terraform import frostmoln_security_group_rule.<name> \"${frostmoln_security_group.app.id}/<rule_id>\"` " +
"or remove it, then re-run the plan."
)
}
}
resource "frostmoln_security_group" "app" {
name = "app"
delete_default_egress = true
}
resource "frostmoln_security_group_rule" "ssh_ingress" {
security_group_id = frostmoln_security_group.app.id
direction = "ingress"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_cidr = "203.0.113.0/24"
description = "ssh from the office"
}
# The injected egress pair, if it still exists on a group you did not create
# with delete_default_egress = true: import it ONLY IN ORDER TO DESTROY it, and
# remove the block once the destroy has run — leaving it re-creates a rule the
# platform refuses (no rule may be created with an empty remote).
# terraform import frostmoln_security_group_rule.old_egress \
# "${frostmoln_security_group.app.id}/<injected_rule_id>"Schema
Required
security_group_id(String) The ID of the security group whose stored rules to list.
Read-Only
id(String) The identifier of this listing — the same security group id assecurity_group_id, deliberately mirrored rather than distinct: one group's stored rule set is the whole identity of this listing, and there is no server-side id beyond it.rules(Attributes List) Every rule stored on the group, in the rendering the platform reports back: exactly the rowsfrostmoln_security_group_rulecan see, including rows nothing in the configuration declares. A group with no rules renders as an empty list, never null. Row order is the platform's reporting order and is not guaranteed stable — pin on rule ids, never on order. (see below for nested schema)
Nested Schema for rules
Read-Only:
description(String) The description stored on the rule, null when unset.direction(String) The direction of the rule: ingress or egress.id(String) The unique identifier of the rule — the id an import offrostmoln_security_group_ruleaddresses it by.port_range_max(Number) The maximum port number. Null when the rule carries no port range.port_range_min(Number) The minimum port number. Null when the rule carries no port range (ananyoricmprule, or one of the injected allow-all egress rules).protocol(String) The protocol: tcp, udp, icmp, or any.remote_cidr(String) The remote CIDR the rule allows traffic from (ingress) or to (egress). Null when the rule names no CIDR. Whenremote_group_idis ALSO null: unqualified on egress (the injected allow-all pair), world-open on ingress — see WHAT A NULL REMOTE MEANS above.remote_group_id(String) The remote security group the rule allows traffic from (ingress) or to (egress). Null when the rule names no remote group; both remotes null is the caseremote_cidrdescribes.