Configuration¶
LoopProof's onboarding fills in every input. The ExternalIds and the registration token aren't inputs: the module reads them from two SSM parameters, whose ARNs are.
Inputs¶
environment¶
LoopProof's environment, such as staging or production: 1 to 44 lowercase letters, digits or hyphens. It names the resources (loopproof-<environment>-connector, -forwarder, -findings) and selects the control-plane roles that may assume the connector role (loopproof-<environment>-*).
control_plane_account_id¶
LoopProof's control-plane account for that environment. Twelve digits.
external_ids_parameter_arn¶
The ARN of the SSM SecureString parameter that holds the ExternalIds LoopProof presents when it assumes the role, as a JSON list: one value, or two during a rotation, such as ["<ExternalId>"]. Pass aws_ssm_parameter.<name>.arn from Part 1, or the ARN of a parameter you manage another way.
The module reads it with a data source, decrypted, on every plan and apply. Before using the value, it checks it, and fails the run with a message that names the parameter and never shows the value:
- the placeholder
unset: "still holds the placeholder"; - anything but a JSON list of one or two ExternalIds, each 43 characters of
A-Z,a-z,0-9,_and-, none twice.
The validation message for a value that isn't an SSM parameter ARN doesn't repeat it; Terraform shows the value it was given beside the message, as for any input that isn't sensitive.
registration_token_parameter_arn¶
The ARN of the SSM SecureString parameter that holds the registration token (lprt_…) from LoopProof's onboarding. The module reads it as an ephemeral value, so the token is in neither the plan nor the state, and uses it for the one registration. The registration fails, naming the parameter, if it holds the placeholder unset or something that doesn't look like a token. Validated as an SSM parameter ARN like external_ids_parameter_arn.
event_bus_arn¶
LoopProof's EventBridge bus, which receives the forwarded findings. It must be an event bus ARN in the control-plane account.
registration_url¶
LoopProof's registration endpoint, such as https://api.staging.loopproof.ai/connector/registrations. The module POSTs the role's ARN and the Region it's applied in there; see Registration. It must use https://, without credentials in the URL; http:// is accepted only for 127.0.0.1 and localhost, which the module's tests use.
severities¶
Default ["High", "Critical", "Fatal"]. The severities to forward, as Security Hub writes them in the OCSF severity field: Unknown, Informational, Low, Medium, High, Critical, Fatal, Other. Values are case-sensitive; the ASFF labels of Security Hub CSPM, such as HIGH, are rejected.
GuardDuty's findings are forwarded at every severity, whatever this lists; severities decides which of the other products' findings are forwarded, such as Inspector's, CSPM's and exposure findings. See Forwarding.
excluded_accounts¶
Default []. The AWS accounts you keep out of LoopProof, such as an account outside your audit scope or a decommissioned one: twelve digits each, each listed once. Validation messages name the input but don't echo the values.
- The forwarding rule drops their findings, by the finding's account,
cloud.account.uid: they never leave your account. - The module publishes the list, sorted, as JSON in the
Stringparameter/loopproof/<environment>/excluded-accountsin the module's Region, such as["111122223333","444455556666"], or[]when you exclude nothing. LoopProof reads it with the connector role when it proves your account and daily after. The plan shows the list. - A change takes effect on the next apply, which updates the rule and the parameter in place. The order doesn't matter: the module sorts the list.
- EventBridge limits a rule's pattern to 4096 characters, which holds about 250 accounts.
threat_accept_rules and vulnerability_accept_rules¶
Default [] each. The risks you accept, one list per kind, each a rule with its register entry in Linear, which LoopProof applies instead of filing the findings it matches as work; see the example.
threat_accept_rules = [
{
id = "vpn-port-probes"
finding_types = ["Recon:EC2/PortProbeUnprotectedPort"]
where = { tags = { role = "vpn" }, accounts = ["111122223333"] }
reason = "VPN endpoints are public by design"
review_by = "2027-03-31"
decision_ref = "SEC-42"
},
]
vulnerability_accept_rules = [
{
id = "legacy-glibc"
package = { name = "glibc", version = "2.26-64.amzn2.0.1" }
where = { resource_ids = ["i-0123456789abcdef0"] }
reason = "The instance is retired in January"
review_by = "2027-01-31"
decision_ref = "OPS-7"
},
]
| Field | Input | Required | What |
|---|---|---|---|
id | both | yes | 1 to 64 lowercase letters, digits or hyphens, starting with a letter or digit; no other rule's, in either input |
reason | both | yes | Why the risk is accepted; not blank |
review_by | both | yes | A date, YYYY-MM-DD, after which the rule stops applying unless a change renews it |
decision_ref | both | yes | The register entry's Linear issue identifier, such as SEC-42 |
finding_types | threats | yes | GuardDuty's finding types, each in full, such as Recon:EC2/PortProbeUnprotectedPort |
cves | vulnerabilities | this or package | CVE identifiers, such as CVE-2024-12797 |
package | vulnerabilities | this or cves | A source package's name and version, each in full |
where.resource_ids | both | one of the selectors | Resources by their ids |
where.tags | both | one of the selectors | Resources by their tags, a map |
where.image_repositories | vulnerabilities | one of the selectors | Container images by their repositories |
where.accounts | both | no | 12-digit account IDs, which narrow the selectors and never stand alone |
behavioral_exception | threats | no | false unless set |
- The input a rule is in gives its kind: the module publishes
threat_responsefor a rule ofthreat_accept_rulesandvuln_remediationfor one ofvulnerability_accept_rules. - The module checks each value at plan: the fields above, each list without a value twice, and an id used twice, in one input or across both. Each message names the input and the ids of the rules it refuses, and no other value of theirs. Terraform drops an attribute the input's type doesn't name without an error, so a misspelled field, or a field of the other kind, is missing from the rule's published value, which the plan shows.
- The module publishes each rule as JSON in the
Stringparameter/loopproof/<environment>/accept-rules/<id>, in the module's Region, with its fields sorted by name, its lists sorted, and the fields it doesn't set left out, so the same rule always publishes the same bytes. Reordering rules, fields or lists plans no change. A rule must fit in a Standard parameter's 4096 characters as JSON, as SSM counts them: an emoji counts twice, and a combining accent on its own. - LoopProof judges what a rule means when it loads it: the register entry in Linear, the behavioral denylist and an exception rule's limits. See Accepting risks in the README.
Outputs¶
| Output | Description |
|---|---|
role_arn | ARN of the connector role, to paste with region if the registration call failed; see below. |
region | The module's Region, where LoopProof reads the findings, to paste with role_arn; see below. |
role_name | loopproof-<environment>-connector |
event_rule_arn | ARN of the EventBridge rule |
excluded_accounts_parameter | /loopproof/<environment>/excluded-accounts, which publishes excluded_accounts |
accept_rules_parameter_path | /loopproof/<environment>/accept-rules, under which each rule of both inputs is a parameter |
Before LoopProof has proven your account, you can paste role_arn and region into LoopProof's onboarding if the registration call failed. Once it has, LoopProof takes no paste.
No output contains an ExternalId or the token. 0.3.0's registered output is gone: an apply that finishes has registered the role, since a refused registration fails it.
Registration¶
The module reports the role's ARN and the AWS provider's Region to registration_url, once: when its terraform_data.registration resource is created, on the apply that creates the role. A local-exec provisioner runs files/register.sh with /bin/sh, which POSTs {"role_arn": "...", "region": "..."} with curl. LoopProof reads your Security Hub findings in that Region; an IAM role ARN names no Region, so the call carries it.
- The token reaches the script through the provisioner's environment, from the ephemeral read, and
curlthrough its standard input; it is on no command line, in the plan or in the state. curl -sS --fail-with-body --max-time 30. The script checkscommand -v curlfirst, andcurl --versionfor 7.76.0 or later, which added--fail-with-body, and fails with a plain message if either is wrong.- A 2xx answer registers. A 4xx fails at once; connection errors, timeouts and 5xx answers are tried again, four attempts in all, two, four and eight seconds apart.
- Later plans and applies make no call. The call runs again when the role is replaced, and with
terraform apply -replace=module.<name>.terraform_data.registration. - A failed call fails the apply and leaves the resource tainted, so the next plain apply calls again.
While the script runs, Terraform shows its output as (output suppressed due to ephemeral value in config), because its environment holds the token. When it fails, the error shows all it printed, and never the request or the token:
Error: local-exec provisioner error
Error running command '.terraform/modules/loopproof_connector/files/register.sh':
exit status 1. Output: LoopProof refused the connector registration: HTTP/2 401
LoopProof's answer: {"detail": "..."}
Get a new registration token in LoopProof and put it in the SSM parameter
arn:aws:ssm:us-east-1:111122223333:parameter/loopproof/production/registration-token, then apply again.
Before LoopProof has proven your account, a new registration token comes from LoopProof's onboarding.
Once it has, an admin of your tenant mints one on LoopProof's Settings page.
A minted token registers only the role your tenant already has.
Before LoopProof has proven your account, you can paste the connector role ARN and the Region into
LoopProof's onboarding instead:
Role ARN: arn:aws:iam::111122223333:role/loopproof-production-connector
Region: us-east-1
Once LoopProof has proven your account, it takes no paste: the registration needs a registration token
and a machine that reaches LoopProof.
The script can't tell whether LoopProof has proven your account, so it names both cases. Every failure ends with the paste and its condition; a failure that may need a new token (a 4xx, the placeholder, or a value that isn't a token) also says where one comes from.
Tags¶
Every resource gets created_by_module = "infrahouse/loopproof-connector/aws" and loopproof_environment = <environment>; the connector role also gets module_version. Add your own with the AWS provider's default_tags.