Fix deploy and make it general - #124
Conversation
…deploys Remove personal defaults (domain, AWS profile, state bucket, GitHub repo fork URL) from Terraform variables so other deployers can fork and apply with their own values via terraform.tfvars, without editing .tf files. - Add project_name variable driving Lightsail resource naming - Convert infra/main/backend.tf to a partial S3 backend config (bucket/key/region now supplied via backend.hcl, gitignored) - Widen bootstrap IAM policy Resource to "*" so the JSON works unedited for any deployer's own AWS account - Parameterize CI deploy host/health-check URL via GitHub Actions repository variables (SSH_HOST, API_HEALTH_URL) - Add terraform.tfvars.example / backend.hcl.example templates Verified zero-diff against the real production state: terraform plan output is identical before and after this change (bootstrap: "No changes"; main: same pre-existing drift, unrelated to this refactor).
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. 📝 WalkthroughWalkthroughThe PR generalizes bootstrap and main Terraform configuration for fork deployments. It externalizes backend settings, parameterizes resource names and deployment inputs, broadens bootstrap S3 policy resources, and configures CI to read deployment endpoints from repository variables. ChangesFork deployment infrastructure
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
.github/workflows/ci.yml (1)
151-151: 🔒 Security & Privacy | 🔵 TrivialProtect the configurable SSH target.
vars.SSH_HOSTnow controls whereappleboy/ssh-action@v1executessudo /opt/foundit/redeploy.sh. A changed value can send deployment commands to the wrong machine.Store the target and expected host fingerprint in a protected deployment environment. Confirm strict host-key verification for the selected action version before enabling fork deployments.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml at line 151, Update the deployment job using appleboy/ssh-action@v1 to source the SSH target and expected host fingerprint from a protected deployment environment rather than an unrestricted vars.SSH_HOST value. Configure strict host-key verification with the action’s supported fingerprint option, and ensure fork-triggered deployments remain disabled until that protected configuration is enforced.infra/main/backend.tf (1)
10-11: 🩺 Stability & Availability | 🔵 TrivialEnable state locking for the S3 backend.
Neither this backend block nor
infra/main/backend.hcl.exampleenablesuse_lockfile. HashiCorp documents this option as disabled by default. Concurrent local and CI applies can update the same state without coordination. Enable locking for the declared Terraform version, grant exact.tflockpermissions, and verify that the bootstrap bucket has versioning and encryption enabled. (developer.hashicorp.com)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@infra/main/backend.tf` around lines 10 - 11, Update the S3 backend configuration and backend.hcl.example to enable use_lockfile for the declared Terraform version. Grant the backend role exact read/write/delete permissions for the .tflock object, and verify the bootstrap bucket has versioning and encryption enabled.Source: MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Line 162: Update the workflow step containing the curl health check to pass
API_HEALTH_URL through the step’s env configuration, then reference the quoted
shell variable in the curl command instead of interpolating the GitHub
expression in the script. Preserve the existing curl options and conditional
behavior.
In `@infra/bootstrap/policies/foundit-bootstrap-policy.json`:
- Line 8: Scope the S3 statement in
infra/bootstrap/policies/foundit-bootstrap-policy.json at line 8 to the intended
state-bucket ARN instead of a global resource. Review
infra/bootstrap/policies/foundit-deploy-policy.json lines 8-14 for the
corresponding S3 scope and align it if it grants the same access; update
infra/bootstrap/README.md at lines 15-21, 33-35, and 56-58 so its permissions
and “exactly needed” claims accurately describe the resulting policy.
In `@infra/main/backend.tf`:
- Around line 1-3: Update the deploy policy referenced by FounditDeployPolicy so
its S3 permissions no longer use wildcard resources: scope s3:ListBucket to the
bootstrap state bucket ARN, and scope s3:GetObject, s3:PutObject, and
s3:DeleteObject to the configured Terraform state-key and lock-file ARNs,
retaining delete access only for the lock object.
In `@infra/main/README.md`:
- Around line 42-46: Update the README instructions for
github_deploy_key_private to show a parseable HCL heredoc value in
terraform.tfvars, including the private key content between heredoc delimiters.
Keep the existing read-only deploy-key guidance and warning against using a
personal SSH key.
In `@infra/main/variables.tf`:
- Around line 14-18: Update the project_name variable validation to reject
values that produce invalid Lightsail resource names when used with the existing
instance, key pair, and static IP suffixes, while preserving the current
default. Extend the variable description to state that project_name must be
unique within the shared AWS account and region.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Line 151: Update the deployment job using appleboy/ssh-action@v1 to source the
SSH target and expected host fingerprint from a protected deployment environment
rather than an unrestricted vars.SSH_HOST value. Configure strict host-key
verification with the action’s supported fingerprint option, and ensure
fork-triggered deployments remain disabled until that protected configuration is
enforced.
In `@infra/main/backend.tf`:
- Around line 10-11: Update the S3 backend configuration and backend.hcl.example
to enable use_lockfile for the declared Terraform version. Grant the backend
role exact read/write/delete permissions for the .tflock object, and verify the
bootstrap bucket has versioning and encryption enabled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 786480c6-67aa-4a69-af37-54cbd315c669
📒 Files selected for processing (13)
.github/workflows/ci.yml.gitignoreinfra/bootstrap/README.mdinfra/bootstrap/policies/foundit-bootstrap-policy.jsoninfra/bootstrap/policies/foundit-deploy-policy.jsoninfra/bootstrap/terraform.tfvars.exampleinfra/bootstrap/variables.tfinfra/main/README.mdinfra/main/backend.hcl.exampleinfra/main/backend.tfinfra/main/main.tfinfra/main/terraform.tfvars.exampleinfra/main/variables.tf
| run: | | ||
| for attempt in 1 2 3 4 5 6; do | ||
| if curl -fsS --max-time 10 https://foundit-api.garychang1214.com/api/health; then | ||
| if curl -fsS --max-time 10 "${{ vars.API_HEALTH_URL }}"; then |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow excerpt =="
sed -n '130,175p' .github/workflows/ci.yml || true
echo
echo "== security context references =="
rg -n "permissions:|id-token:|write|API_HEALTH_URL|vars\.|contents:" .github/workflows/ci.yml .github/workflows || true
echo
echo "== deterministic bash probe for interpolation vs env variable =="
python3 - <<'PY'
import subprocess
bad = 'API_HEALTH_URL="http://example.com"; x="x"; if curl -fsS --max-time 10 "${API_HEALTH_URL}"; then echo first; echo second; fi ;; >/tmp/bad_interpolation.log 2>&1 ; echo "bad_interpolation: $?"; cat /tmp/bad_interpolation.log'
# The intended pattern cannot be directly simulated in a shell one-liner,
# so parse it:
good = 'API_HEALTH_URL="http://example.com"; curl -fsS --max-time 10 -- "$API_HEALTH_URL" ;; >/tmp/good_env.log 2>&1 ; echo "good_env: $?"; cat /tmp/good_env.log'
for name, cmd in [("imagine_interpolated_bad", bad), ("intended", good)]:
try:
out = subprocess.run(["bash", "-c", "set +e; " + cmd], text=True, capture_output=True, timeout=10)
print(f">{name} exit={out.returncode}")
print(out.stdout.strip())
if out.stderr.strip():
print(out.stderr.strip())
except Exception as e:
print(f">{name} exception={e}")
PYRepository: 86unj/Foundit
Length of output: 2683
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deterministic bash command injection probe =="
python3 - <<'PY'
import subprocess
payload = '"; echo INJECTED; echo SUCCESS;; # '
cases = {
"curl_with_curly_var_substitution": 'API_HEALTH_URL="http://example.com"; if bash -c "curl -fsS --max-time 10 \'"${API_HEALTH_URL}"\'"; then echo first; echo second; fi ;;',
"curl_with_quoted_envvar": 'API_HEALTH_URL="http://example.com"; curl -fsS --max-time 10 -- "$API_HEALTH_URL" ;;',
"bad_escaping_newline_payload": 'API_HEALTH_URL="http://example.com"; if curl -fsS --max-time 10 "${API_HEALTH_URL}' + payload + '"; then echo first; echo second; fi ;;',
}
for name, cmd in cases.items():
print(f"\n== {name} ==")
# Use file descriptor passing to avoid shell quoting in the probe construction.
fd = 3
out = subprocess.run(["bash", "-c", cmd], text=True, capture_output=True, timeout=10)
print(f"exit={out.returncode}")
print("stdout:", repr(out.stdout))
print("stderr:", repr(out.stderr))
PY
echo
echo "== workflow top-level permissions =="
sed -n '1,40p' .github/workflows/ci.yml || trueRepository: 86unj/Foundit
Length of output: 1849
🌐 Web query:
GitHub Actions run context shell context template variables inject run script before execution documentation
💡 Result:
In GitHub Actions, interpolating context variables directly into a 'run' step using the '${{ }}' syntax is a critical security risk known as shell injection [1][2][3]. When you use the '${{ }}' expression syntax inside a 'run' block, the GitHub Actions template engine evaluates the expression and performs a literal text substitution before the script is passed to the runner's shell [1][3][4]. If the context variable contains untrusted input—such as an issue title, pull request description, or branch name—an attacker can craft malicious input that breaks out of the intended command context to execute arbitrary code on the runner [1][2][4]. To safely handle untrusted input or context variables in shell scripts, you must use intermediate environment variables [5][2][4]. Recommended Pattern: 1. Define an environment variable within the 'env' block of the specific step [5][2][4]. 2. Assign the context expression to that environment variable [5][4]. 3. Reference the environment variable in your 'run' script using standard shell syntax (e.g., '$VAR_NAME') [5][2][4]. Example of the safe pattern: - name: Print PR title safely env: # Assign the untrusted context to an environment variable PR_TITLE: ${{ github.event.pull_request.title }} run: | # Reference as a shell variable; the shell treats this as data, not code echo "$PR_TITLE" By moving the value into an environment variable, the runner receives the content as data rather than executable script, which mitigates the risk of shell metacharacter interpretation [2][3][4]. This approach applies to 'run' commands and also when passing inputs to 'actions/github-script' or similar tools [6][4][7]. While self-hosted runners allow for custom pre-job or setup scripts on the host machine to enforce security policies globally [8], defining step-level environment variables remains the standard and most effective practice for preventing injection vulnerabilities within workflow YAML files [2][4].
Citations:
- 1: https://docs.github.com/en/actions/concepts/security/script-injections
- 2: https://orbisappsec.com/blog/github-actions-shell-injection-how-context-variables-can-compromise-your-cicd-pipeline
- 3: https://sebastion.dev/intelligence/2026-03-21-poc-github-actions-shell-injection-via-unsanitized-issue-metadata
- 4: https://www.thinkdifferent.blog/courses/github-actions-cicd/bonus/script-injection-and-pull-request-target/
- 5: https://github.blog/security/supply-chain-security/four-tips-to-keep-your-github-actions-workflows-secure/
- 6: https://github.com/actions/github-script?tab=readme-ov-file
- 7: https://github.com/actions/github-script
- 8: Exec a custom pre-job script before executing a workflow actions/runner#2085
🌐 Web query:
Appleboy ssh-action permissions needed secrets LIGHTSAIL_SSH_KEY
💡 Result:
To use AWS Lightsail SSH keys with appleboy/ssh-action, you do not need special file system permissions, but you must correctly configure the SSH key content within GitHub Secrets and ensure your Lightsail instance is properly prepared [1][2][3]. Key Requirements and Configuration: 1. GitHub Secrets Setup: Store your private SSH key in a GitHub Repository Secret (e.g., named LIGHTSAIL_SSH_KEY) [3][4]. The content should be the raw private key text [1][5]. Ensure there are no leading or trailing spaces in the secret value, as these can cause authentication failures [6]. 2. Lightsail Configuration: The Lightsail instance must have the corresponding public key authorized [7]. - If you are using the default key downloaded from AWS Lightsail, ensure it is correctly formatted [3]. - If you are generating a new key pair, it is recommended to use ed25519 for better compatibility and security [2][5]. - Ensure the public key is added to the ~/.ssh/authorized_keys file on the remote Lightsail instance [2][7]. - Standard file permissions on the target server for SSH are: -.ssh directory: 700 [8][5] - authorized_keys file: 640 or 600 [8][5] 3. Action Configuration: In your GitHub Actions workflow YAML file, map the secret to the key input [1][4]: - name: Deploy to Lightsail uses: appleboy/ssh-action@v1 with: host: ${{ secrets.LIGHTSAIL_IP }} username: ${{ secrets.LIGHTSAIL_USER }} key: ${{ secrets.LIGHTSAIL_SSH_KEY }} port: 22 Troubleshooting Tips: - If authentication fails, verify that you are using the correct static IP address for your Lightsail instance rather than a hostname [9]. - Test the SSH connection from your local machine using the same private key before attempting to use it in GitHub Actions to confirm the key pair is valid [7]. - If the connection times out, ensure the instance's firewall allows traffic on port 22 from the GitHub Actions runner IP range (or generally, if public access is allowed) [9].
Citations:
- 1: https://github.com/appleboy/ssh-action?tab=readme-ov-file
- 2: https://richardryu.com/portfolio/github-actions.html
- 3: https://levelup.gitconnected.com/deploying-a-react-vite-app-on-aws-lightsail-with-github-github-actions-and-ssl-certificate-2d903beee2f5
- 4: https://github.com/knightsri/VenturePulse/blob/222ec7d992cd87cf06b6ef8e5adde27388614d48/.github/workflows/deploy.yml
- 5: https://github.com/appleboy/ssh-action/tree/master?tab=readme-ov-file
- 6: Using private SSH keys from github secrets does not work appleboy/ssh-action#349
- 7: SSH key works from my local machine, but not with this Action. appleboy/ssh-action#7
- 8: https://github.com/appleboy/ssh-action
- 9: https://stackoverflow.com/questions/75158833/github-action-deploying-to-lightsail-has-suddenly-started-failing-with-dial-tcp
🌐 Web query:
appleboy ssh-action v1 key input private key SSH private key
💡 Result:
To use an SSH private key with the appleboy/ssh-action (v1), you provide the raw content of your private key directly to the key input parameter [1][2]. ### Implementation Steps 1. Prepare your private key: Copy the entire contents of your private key file (e.g., ~/.ssh/id_rsa), ensuring you include the header and footer lines, such as -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY----- [2][3]. 2. Store as a GitHub Secret: Go to your repository settings on GitHub, navigate to Secrets and variables > Actions, and add your private key as a new repository secret (e.g., named SSH_PRIVATE_KEY) [1][4]. 3. Configure your workflow: Use the key parameter in your workflow YAML file to reference the secret [1][2]. Example configuration: - name: Execute remote SSH commands using SSH key uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.SSH_PRIVATE_KEY }} port: ${{ secrets.PORT }} script: whoami ### Important Considerations * Passphrase: If your private key is protected by a passphrase, you must also provide the passphrase input parameter [2][5]. - name: Execute with SSH key and passphrase uses: appleboy/ssh-action@v1 with: key: ${{ secrets.SSH_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} #... other parameters * Security: Never commit your private key directly to your repository [2][3]. Always use GitHub Secrets to keep the key encrypted and secure [1][4]. * Troubleshooting: If you encounter authentication failures, ensure that the entire key content, including the start and end markers, is copied correctly without extra quotes or leading/trailing whitespace in the GitHub Secret value [4].
Citations:
- 1: https://github.com/appleboy/ssh-action?tab=readme-ov-file
- 2: https://github.com/appleboy/ssh-action/tree/refs/tags/v1.2.4
- 3: https://github.com/appleboy/ssh-action/tree/master?tab=readme-ov-file
- 4: SSH Key appleboy/ssh-action#6
- 5: https://github.com/marketplace/actions/ssh-remote-commands?version=v1.0.0
Do not interpolate API_HEALTH_URL into the deploy runner.
GitHub substitutes ${{ vars.API_HEALTH_URL }} into the run script before Bash parses it, so quotes or newlines in the repository variable can break command syntax and inject code. Use the suggested env pattern and quote the shell variable.
🧰 Tools
🪛 zizmor (1.28.0)
[info] 162-162: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml at line 162, Update the workflow step containing
the curl health check to pass API_HEALTH_URL through the step’s env
configuration, then reference the quoted shell variable in the curl command
instead of interpolating the GitHub expression in the script. Preserve the
existing curl options and conditional behavior.
Source: Linters/SAST tools
| "arn:aws:s3:::foundit-tfstate-688948287774", | ||
| "arn:aws:s3:::foundit-tfstate-688948287774/*" | ||
| ] | ||
| "Resource": "*" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
for policy in \
infra/bootstrap/policies/foundit-bootstrap-policy.json \
infra/bootstrap/policies/foundit-deploy-policy.json
do
jq -e '
def actions:
.Action | if type == "array" then . else [.] end;
all(.Statement[];
if ((actions | any(startswith("s3:"))) and .Resource == "*") then
(actions | all(. == "s3:CreateBucket"))
else
true
end
)
' "$policy" >/dev/null
doneRepository: 86unj/Foundit
Length of output: 151
🏁 Script executed:
#!/usr/bin/env/bash
set -euo pipefail
echo "Files:"
git ls-files 'infra/bootstrap/policies/*.json' 'infra/bootstrap/README.md' || true
echo
echo "Bootstrap policy:"
cat -n infra/bootstrap/policies/foundit-bootstrap-policy.json
echo
echo "Deploy policy:"
cat -n infra/bootstrap/policies/foundit-deploy-policy.json
echo
echo "README relevant lines:"
sed -n '1,70p' infra/bootstrap/README.md | cat -n
echo
echo "Broad S3 resource/statements probe:"
python3 - <<'PY'
import json, pathlib
for p in pathlib.Path('infra/bootstrap/policies').glob('*.json'):
data=json.loads(p.read_text())
print(f'--- {p} ---')
for i,st in enumerate(data.get('Statement') or [], 1):
action=st.get('Action')
resource=st.get('Resource')
print(f'Statement {i}: Effect={st.get("Effect")}, Action={action!r}, Resource={resource!r}')
PYRepository: 86unj/Foundit
Length of output: 6782
Scope the bootstrap S3 policy to the state bucket and update the README.
infra/bootstrap/policies/foundit-bootstrap-policy.json grants s3:* on *, and the README says this gives the deploy user exactly the S3 permissions needed to create the state bucket. Scope the S3 statement to the intended bucket ARN, or clearly separate the unavoidable wildcard actions from the “exactly needed” claim.
📍 Affects 3 files
infra/bootstrap/policies/foundit-bootstrap-policy.json#L8-L8(this comment)infra/bootstrap/policies/foundit-deploy-policy.json#L8-L14infra/bootstrap/README.md#L15-L21infra/bootstrap/README.md#L33-L35infra/bootstrap/README.md#L56-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/bootstrap/policies/foundit-bootstrap-policy.json` at line 8, Scope the
S3 statement in infra/bootstrap/policies/foundit-bootstrap-policy.json at line 8
to the intended state-bucket ARN instead of a global resource. Review
infra/bootstrap/policies/foundit-deploy-policy.json lines 8-14 for the
corresponding S3 scope and align it if it grants the same access; update
infra/bootstrap/README.md at lines 15-21, 33-35, and 56-58 so its permissions
and “exactly needed” claims accurately describe the resulting policy.
| # Remote state lives in the bucket created by ../bootstrap. The deploy IAM | ||
| # user (via FounditGroup -> FounditDeployPolicy) already has read/write on | ||
| # every object in this bucket, so no IAM change is needed for this state key. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Tracked files matching relevant names:\n'
git ls-files | rg '(^|/)(backend\.hcl|backend\.tf|foundit-deploy-policy\.json)$|policies/' || true
printf '\n--- infra/main/backend.tf ---\n'
if [ -f infra/main/backend.tf ]; then
cat -n infra/main/backend.tf
fi
printf '\n--- infra/bootstrap/policies/foundit-deploy-policy.json ---\n'
if [ -f infra/bootstrap/policies/foundit-deploy-policy.json ]; then
cat -n infra/bootstrap/policies/foundit-deploy-policy.json
fi
printf '\n--- Search for foundit deploy policy and deploy user/policy references ---\n'
rg -n "FounditDeployPolicy|foundit-deploy-policy|s3:|Resource":.*\*|Backend|terraform backend|backend" . || true
printf '\n--- Policy bucket objects extraction ---\n'
python3 - <<'PY'
import json, pathlib, re
p=pathlib.Path("infra/bootstrap/policies/foundit-deploy-policy.json")
if p.exists():
data=json.loads(p.read_text())
policy=data.get("Policy") if isinstance(data, dict) and "Policy" in data else data
print(json.dumps(policy, indent=2, sort_keys=True))
PYRepository: 86unj/Foundit
Length of output: 1921
🌐 Web query:
HashiCorp Terraform S3 backend required permissions bucket actions object actions developer.hashicorp.com
💡 Result:
The AWS IAM permissions required for the Terraform S3 backend depend on your specific configuration, such as whether you use workspaces or the lockfile feature [1][2]. When not using workspaces (or using only the default workspace), the minimum required permissions are [1][2]: - s3:ListBucket: Must be granted on the bucket (e.g., arn:aws:s3:::mybucket) to list the path where the state is stored [1][2]. - s3:GetObject: Must be granted on the state file path (e.g., arn:aws:s3:::mybucket/path/to/my/key) [1][2]. - s3:PutObject: Must be granted on the state file path [1][2]. Note that s3:DeleteObject is generally not required for the state file itself because Terraform does not delete the state file [1][2]. However, if the use_lockfile configuration option is enabled, s3:DeleteObject is required to manage the lock file (e.g., arn:aws:s3:::mybucket/path/to/my/key.tflock) [1][2][3]. When using use_lockfile, s3:GetObject and s3:PutObject permissions are also required for the lock file [1][2]. When using workspaces, Terraform requires additional permissions to perform operations on workspace-specific state files [1][2]: - s3:ListBucket: Must be able to list the paths for both the default workspace and other workspaces [1][2]. - s3:GetObject, s3:PutObject, s3:DeleteObject: Must be granted on both the default state path and the workspace-specific paths (e.g., arn:aws:s3:::mybucket/<workspace_key_prefix>/*/path/to/my/key) [1][2]. If you are using DynamoDB for state locking, separate IAM permissions are required for the DynamoDB table, typically including actions such as dynamodb:GetItem, dynamodb:PutItem, and dynamodb:DeleteItem [4]. If your S3 bucket uses server-side encryption with KMS, you must also grant the necessary kms:Decrypt and kms:GenerateDataKey permissions to the IAM principal [5].
Citations:
- 1: https://developer.hashicorp.com/terraform/language/backend/s3
- 2: https://docs.hashicorp.com/terraform/language/backend/s3
- 3: s3:DeleteObject IAM permission on S3 backend hashicorp/terraform#36175
- 4: https://developer.hashicorp.com/terraform/language/v1.5.x/settings/backends/s3
- 5: https://devopsboys.com/blog/terraform-s3-access-denied-fix-2026
Scope S3 permissions to the configured state bucket.
The deploy policy grants s3:ListBucket, s3:GetObject, s3:PutObject, and s3:DeleteObject with "Resource": "*", which gives the deploy identity access to all S3 buckets and objects it can use. The Terraform S3 backend only requires bucket-specific permissions for s3:ListBucket, object-specific permissions for the state key, and s3:DeleteObject only for the lock file when locking is enabled. Scope these actions to the bootstrap state bucket ARN for list access, and to the configured state and lock key ARNs for object access.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/main/backend.tf` around lines 1 - 3, Update the deploy policy
referenced by FounditDeployPolicy so its S3 permissions no longer use wildcard
resources: scope s3:ListBucket to the bootstrap state bucket ARN, and scope
s3:GetObject, s3:PutObject, and s3:DeleteObject to the configured Terraform
state-key and lock-file ARNs, retaining delete access only for the lock object.
Source: MCP tools
| Add `foundit-deploy-key.pub` to your fork under **Settings → Deploy keys | ||
| → Add deploy key** (leave "Allow write access" unchecked — read-only). | ||
| Put the contents of `foundit-deploy-key` (the private half) into | ||
| `terraform.tfvars` as `github_deploy_key_private`. Don't reuse your | ||
| personal SSH key for this. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n -C 3 'github_deploy_key_private|<<-' infra/main/README.mdRepository: 86unj/Foundit
Length of output: 568
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== tfvars examples / key variable references =="
rg -n -C 2 'github_deploy_key_private|terraform\.tfvars|decrypt|private|private_key' infra -g '!*~*' || true
echo
echo "== relevant infra files =="
find infra -maxdepth 3 -type f | sed 's#^\./##' | sort | head -200
echo
echo "== README surrounding lines =="
sed -n '35,55p' infra/main/README.mdRepository: 86unj/Foundit
Length of output: 10474
🌐 Web query:
Terraform HCL heredoc strings multiline with triple-hyphen and end label
💡 Result:
In Terraform HCL, heredoc strings provide a way to express multiline strings [1][2]. They are defined by an opening marker followed by a user-chosen identifier, and they are terminated when that same identifier appears alone on a line [1][3]. The standard heredoc is introduced by the << sequence [1][4]. In this mode, every character, including leading spaces, is treated as a literal part of the string [1][4]. The indented heredoc is introduced by the <<- sequence [1][4]. This variant is designed to improve code readability by allowing the heredoc block to be indented within the HCL code [1][2]. When using this syntax, Terraform analyzes the content lines to determine the smallest number of leading spaces and automatically strips that many spaces from the beginning of every line in the string [1][5]. Key rules for termination and syntax: - Delimiter: You choose an identifier (e.g., EOT, EOF) which must be used as both the opening reference and the closing line [1][4]. - Closing line: The identifier must appear alone on its own line [1][4]. For the <<- variant, the closing identifier may have arbitrary leading spaces (indentation) [3]. - Trailing spaces: The closing line must not contain trailing spaces after the identifier, or the heredoc will fail to terminate [6]. - Indentation stripping: Only the <<- syntax performs indentation stripping [1][5]. Standard << does not [1][4]. It is important to note that while template interpolations (e.g., ${var}) and directives (e.g., %{for...}) work within heredoc strings, complex interactions between indentation stripping and template directives can sometimes result in unexpected whitespace in the final output [7][5].
Citations:
- 1: https://docs.hashicorp.com/terraform/language/expressions/strings
- 2: https://developer.hashicorp.com/terraform/language/expressions/strings
- 3: https://github.com/juliosueiras/terraform-lsp/blob/master/vendor/github.com/hashicorp/hcl/v2/hclsyntax/spec.md
- 4: https://github.com/hashicorp/web-unified-docs/blob/main/content/terraform/v1.14.x/docs/language/expressions/strings.mdx
- 5: https://www.env0.com/blog/terraform-strings-interpolation-heredoc-functions
- 6: HEREDOC doesn't terminate correctly hashicorp/terraform#4835
- 7: stripmarkers and indented heredoc hashicorp/terraform#23710
🌐 Web query:
Terraform HCL strings escape newline \n inside double quotes
💡 Result:
In Terraform HCL, you can represent a newline character inside a double-quoted string by using the escape sequence \n [1][2]. Within double-quoted strings, the backslash character () initiates an escape sequence [1][3]. The following escape sequences are supported for quoted strings [1][4]: \n: Newline [1][2] \r: Carriage return [1][2] \t: Tab [1][2] ": Literal double quote [1][2] \: Literal backslash [1][2] \uNNNN: Unicode character (4 hex digits) [1][2] \UNNNNNNNN: Unicode character (8 hex digits) [1][2] Alternatively, Terraform supports a "heredoc" syntax (e.g., <<EOT... EOT), which allows you to define multi-line strings without needing to escape newlines with backslashes [1][5]. In heredoc strings, backslash sequences are interpreted literally rather than as escape sequences [1][4].
Citations:
- 1: https://developer.hashicorp.com/terraform/language/expressions/strings
- 2: https://docs.hashicorp.com/terraform/language/expressions/strings
- 3: https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md
- 4: https://github.com/hashicorp/web-unified-docs/blob/main/content/terraform/v1.14.x/docs/language/expressions/strings.mdx
- 5: https://spacelift.io/blog/terraform-string
Show a parseable HCL value for github_deploy_key_private.
The README tells users to paste a multiline private key directly into terraform.tfvars, but a quoted HCL string requires escaped newlines. Use a heredoc example instead:
Use an HCL heredoc
- Put the contents of `foundit-deploy-key` (the private half) into `terraform.tfvars` as `github_deploy_key_private`.
+ Put the contents of `foundit-deploy-key` into `terraform.tfvars` with an HCL heredoc:
+
+ github_deploy_key_private = <<-KEY
+ <contents of foundit-deploy-key>
+ KEY🧰 Tools
🪛 LanguageTool
[uncategorized] ~45-~45: The official name of this software platform is spelled with a capital “H”.
Context: ...ate half) into terraform.tfvars as github_deploy_key_private. Don't reuse your ...
(GITHUB)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/main/README.md` around lines 42 - 46, Update the README instructions
for github_deploy_key_private to show a parseable HCL heredoc value in
terraform.tfvars, including the private key content between heredoc delimiters.
Keep the existing read-only deploy-key guidance and warning against using a
personal SSH key.
| variable "project_name" { | ||
| description = "Short name prefixed onto Lightsail resource names, e.g. \"<project_name>-app\"." | ||
| type = string | ||
| default = "foundit" | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '^infra/main/(variables\.tf|main\.tf)$' || true
echo "== variables.tf =="
if [ -f infra/main/variables.tf ]; then
cat -n infra/main/variables.tf
fi
echo "== main.tf outline/sections =="
if [ -f infra/main/main.tf ]; then
wc -l infra/main/main.tf
cat -n infra/main/main.tf
fi
echo "== project_name usages =="
rg -n '\bproject_name\b|aws_lightsail|StaticIp|PublicKey' infra/main || trueRepository: 86unj/Foundit
Length of output: 11966
🌐 Web query:
AWS Lightsail instance name valid characters length key pair name valid characters name static IP name valid characters length
💡 Result:
In Amazon Lightsail, resources such as instances, key pairs, and static IPs generally follow consistent naming requirements. For Amazon Lightsail resource names (including instances and static IPs), the following constraints apply [1][2]: Length: Must be between 2 and 255 characters [1][2]. Characters: Can include alphanumeric characters, numbers, periods (.), dashes (-), and underscores (_) [1][2]. Formatting: Must start and end with an alphanumeric character or number [1][2]. Uniqueness: Must be unique within each AWS Region in your Lightsail account [1][2]. Technical API specifications for these resources often cite the pattern \w[\w\-]*\w [3][4][5], which aligns with these requirements (where \w typically encompasses alphanumeric characters and underscores, often including dashes depending on the implementation context). While some technical documentation may show slight variations in pattern notation, the explicit guidance provided in the AWS user documentation confirms the 2-255 character limit and the specific allowed characters listed above [1][2]. Key pair names are also typically subject to these naming conventions, as they are managed as named resources within the Lightsail environment [4][6]. Top results: [1][2][3][4]
Citations:
- 1: https://docs.aws.amazon.com/lightsail/latest/userguide/lightsail-create-static-ip.html
- 2: https://docs.aws.amazon.com/lightsail/latest/userguide/how-to-create-larger-instance-from-snapshot-using-console.html
- 3: https://docs.aws.amazon.com/lightsail/2016-11-28/api-reference/API_Instance.html
- 4: https://docs.aws.amazon.com/cli/latest/reference/lightsail/create-key-pair.html
- 5: https://docs.aws.amazon.com/lightsail/2016-11-28/api-reference/API_AllocateStaticIp.html
- 6: https://docs.aws.amazon.com/lightsail/2016-11-28/api-reference/API_CreateKeyPair.html
Validate project_name before interpolating it into Lightsail names.
project_name is interpolated into instance, key pair, and static IP names without checks. Add validation so Terraform rejects names that would violate Lightsail naming rules after concatenation. Also document that each project_name must be unique in the shared AWS account and region.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@infra/main/variables.tf` around lines 14 - 18, Update the project_name
variable validation to reject values that produce invalid Lightsail resource
names when used with the existing instance, key pair, and static IP suffixes,
while preserving the current default. Extend the variable description to state
that project_name must be unique within the shared AWS account and region.
Source: MCP tools
Summary by CodeRabbit
New Features
Documentation
Chores