Protecting Astro from Supply Chain Attacks: Part 2 - Long-Term Security Measures

$50 off using BLACKFRIDAY

Sale ends in
11d 12h 54m 25s
LaunchFast Logo LaunchFast

Protecting Astro from Supply Chain Attacks: Part 2 - Long-Term Security Measures

Rishi Raj Jain
Protecting Astro from Supply Chain Attacks Part 2

This is Part 2 of a 3-part series on protecting Astro websites from npm supply chain attacks. Part 1 covered understanding the threat and immediate response. This article focuses on long-term security measures. Check out Part 3 for best practices and monitoring.

High Quality Starter Kits with built-in authentication flow (Auth.js), object uploads (AWS, Clouflare R2, Firebase Storage, Supabase Storage), integrated payments (Stripe, LemonSqueezy), email verification flow (Resend, Postmark, Sendgrid), and much more. Compatible with any database (Redis, Postgres, MongoDB, SQLite, Firestore).
Next.js Starter Kit
SvelteKit Starter Kit

In Part 1, we covered the immediate response to the Shai-Hulud 2.0 attack. Now it’s time to implement comprehensive security measures that will protect your Astro project from future supply chain attacks. These aren’t temporary fixes-they’re essential practices that should become part of your development workflow.

Prerequisites

You’ll need the following:

  • Completed Part 1 of this series

1. Implement Package Lock and Verification

The first line of defense against supply chain attacks is ensuring that you always install exactly the versions of packages you’ve tested and verified.

1.1 Use Exact Version Numbers

Never use version ranges in your package.json for production dependencies:

// ❌ Bad: Allows automatic updates
{
"dependencies": {
"astro": "^4.16.1", // Can install 4.16.x or 4.x.x
"sharp": "~0.33.1" // Can install 0.33.x
}
}
// ✅ Good: Exact versions only
{
"dependencies": {
"astro": "4.16.1",
"sharp": "0.33.1"
}
}

1.2 Configure npm/pnpm for Security

Create an .npmrc file in your project root with strict security settings:

.npmrc
# Always use exact versions when saving packages
save-exact=true
# Ensure package-lock.json is always created and used
package-lock=true
# Run audit on every install
audit=true
# Set minimum audit level (low, moderate, high, critical)
audit-level=moderate
# Fail install if vulnerabilities are found
audit-report=true

For pnpm users (recommended), configure additional security features:

Terminal window
# File: .npmrc (for pnpm)
# Require exact versions
save-exact=true
# Strict peer dependencies
strict-peer-dependencies=true
# Verify package integrity using checksums
package-import-method=copy
# Run security audit on install
audit=true
# Only allow packages from verified registries
registry=https://registry.npmjs.org/
# Verify SSL certificates
strict-ssl=true

1.3 Always Commit Lock Files

Your lock file (package-lock.json or pnpm-lock.yaml) is critical for reproducible builds:

Terminal window
# Ensure lock file is tracked in git
git add pnpm-lock.yaml
git commit -m "chore: commit lock file for security"

Update your .gitignore to ensure lock files are never ignored:

.gitignore
# Make sure these are NOT in .gitignore:
# pnpm-lock.yaml
# package-lock.json
# yarn.lock
# But DO ignore
node_modules/

2. Use Dependency Scanning Tools

Automated scanning tools can detect vulnerabilities before they make it into production. Let’s set up a comprehensive scanning workflow.

2.1 Snyk Security Scan with GitHub Actions

Snyk is a leading developer security platform specializing in scanning your dependencies for vulnerabilities.

Create a GitHub Actions workflow for Snyk scanning:

.github/workflows/snyk-security-scan.yml
name: Snyk Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
snyk-scan:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies (safely)
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Run npm audit
run: pnpm audit --audit-level=moderate
continue-on-error: true
id: npm-audit
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --all-projects
continue-on-error: true
id: snyk-scan
- name: Generate Security Report
if: always()
run: |
echo "# Snyk Security Scan Report" > security-report.md
echo "Generated: $(date)" >> security-report.md
echo "" >> security-report.md
echo "## Scan Results" >> security-report.md
echo "- npm audit: ${{ steps.npm-audit.outcome }}" >> security-report.md
echo "- Snyk scan: ${{ steps.snyk-scan.outcome }}" >> security-report.md
- name: Upload Security Report
uses: actions/upload-artifact@v4
if: always()
with:
name: snyk-security-report-${{ github.run_number }}
path: security-report.md
retention-days: 90

Snyk offers continuous monitoring of your dependencies, keeping track of new vulnerabilities even after your code is merged. It provides fix suggestions that identify exactly which updates will resolve known issues. With strong IDE and CI/CD integration, Snyk helps you catch security problems before your dependencies reach production. Additionally, Snyk features a generous free tier for public and open-source repositories, making it accessible to the broader developer community.

2.2 Trivy Vulnerability Scanner with GitHub Actions

Trivy is an open-source vulnerability scanner that provides broader scanning, including infrastructure and container security.

Set up Trivy with GitHub Actions:

.github/workflows/trivy-scan.yml
name: Trivy Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
jobs:
trivy-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies (safely)
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Trivy vulnerability scan (code & dependencies)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'

Trivy is an open-source security scanner that offers broad coverage for vulnerabilities across your entire project-not just in node_modules, but also inside containers, operating system packages, configuration files, and infrastructure-as-code. Its results can be output in SARIF format, allowing seamless integration with GitHub Security features. Trivy is free, straightforward to automate, and well-suited for both local and CI/CD workflows.

2.3 Set Up Local Security Scanning

Don’t wait for CI/CD to catch issues. Scan locally before pushing:

Terminal window
# Install Snyk CLI globally
npm install -g snyk
# Authenticate with Snyk
snyk auth
# Scan your project
snyk test
# Monitor your project (sends snapshot to Snyk for continuous monitoring)
snyk monitor

Add a pre-push git hook:

Terminal window
# Install husky if you haven't already
pnpm add -D husky
# Initialize husky
npx husky init
# Create pre-push hook
cat > .husky/pre-push << 'EOF'
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "🔍 Running security scan before push..."
pnpm audit --audit-level=moderate
EOF
chmod +x .husky/pre-push

3. Secure Your CI/CD Pipeline

The Shai-Hulud 2.0 attack specifically targeted CI/CD environments where secrets are readily available. Here’s how to harden your pipeline.

3.1 Use Minimal Permissions

Configure your GitHub Actions with the principle of least privilege:

.github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
# Set minimal default permissions
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
# Override only for specific needs
permissions:
contents: read
deployments: write
id-token: write # For OIDC authentication
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
# Critical: Disable install scripts in CI
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Build
env:
# Use environment-specific secrets
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
API_KEY: ${{ secrets.PROD_API_KEY }}
run: pnpm run build
- name: Deploy
run: pnpm run deploy

3.2 Disable Install Scripts

Install scripts are the primary attack vector. Always disable them in CI/CD:

# Always use --ignore-scripts in CI
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

For packages that legitimately need install scripts (like sharp or esbuild), rebuild them explicitly:

scripts/secure-install.ts
import { execSync } from 'child_process'
console.log('🔒 Running secure installation...\n')
try {
// Install without running any scripts
console.log('📦 Installing packages without scripts...')
execSync('pnpm install --frozen-lockfile --ignore-scripts', {
stdio: 'inherit',
})
console.log('\n✅ Dependencies installed safely')
// Explicitly rebuild trusted packages that need native bindings
const TRUSTED_PACKAGES = [
'sharp', // Image processing
'esbuild', // Bundler
'@parcel/watcher', // File watcher
// Add other trusted packages here
]
console.log('\n📦 Rebuilding trusted native packages...')
for (const pkg of TRUSTED_PACKAGES) {
try {
console.log(` - Rebuilding ${pkg}...`)
execSync(`pnpm rebuild ${pkg}`, { stdio: 'inherit' })
console.log(` ✅ ${pkg} rebuilt successfully`)
} catch (error) {
console.error(` ❌ Failed to rebuild ${pkg}`)
// Don't fail the whole process - package might not be installed
}
}
console.log('\n✅ Secure installation complete!')
} catch (error) {
console.error('\n❌ Installation failed:', error.message)
process.exit(1)
}

Update your package.json:

{
"scripts": {
"install:safe": "tsx scripts/secure-install.ts",
"build": "astro build",
"dev": "astro dev"
}
}

3.3 Use Environment Isolation

Use different secrets and credentials for each environment (dev, staging, production) to contain potential breaches and prevent leaks from spreading across environments.

.github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
push:
branches: [develop]
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # GitHub environment
steps:
- uses: actions/checkout@v4
- name: Build and Deploy
env:
# Staging-specific secrets
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
API_KEY: ${{ secrets.STAGING_API_KEY }}
run: |
pnpm install --frozen-lockfile --ignore-scripts
pnpm run build
pnpm run deploy:staging

.github/workflows/deploy-production.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production # Different environment
steps:
- uses: actions/checkout@v4
- name: Build and Deploy
env:
# Production-specific secrets
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
API_KEY: ${{ secrets.PROD_API_KEY }}
run: |
pnpm install --frozen-lockfile --ignore-scripts
pnpm run build
pnpm run deploy:production

3.4 Use OIDC Instead of Long-Lived Tokens

For cloud deployments, use OpenID Connect (OIDC) instead of storing long-lived credentials:

.github/workflows/deploy-aws.yml
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Use OIDC to get temporary credentials
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
# Now you have temporary AWS credentials
- name: Deploy to S3
run: |
pnpm run build
aws s3 sync dist/ s3://my-bucket/

By adopting OIDC, you eliminate the need to store long-lived cloud credentials in GitHub, significantly reducing the risk if your CI environment is ever compromised. Instead, GitHub Actions can request short-lived, automatically expiring credentials for each workflow run. Every authentication event is tracked and auditable in AWS CloudTrail, ensuring full traceability and security transparency.

4. Configure npm Audit Automation

Make security auditing a regular part of your development workflow.

4.1 Set Up Pre-Commit Hooks

Setting up pre-commit hooks adds an automated checkpoint in your workflow to prevent you from committing insecure code. Every time you attempt a commit, Husky will trigger the security audit (using pnpm audit --audit-level=moderate). If vulnerabilities above the threshold are detected, the commit is blocked until the issues are resolved or bypassed intentionally. This helps ensure only secure code is committed to the repository, maintaining your project’s integrity from the outset.

.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "🔍 Running pre-commit security checks...\n"
# Run audit
echo "📦 Checking for vulnerabilities..."
pnpm audit --audit-level=moderate
if [ $? -ne 0 ]; then
echo "\n❌ Security vulnerabilities found!"
echo "Run 'pnpm audit fix' to resolve issues."
echo "Or use --no-verify to skip (not recommended)."
exit 1
fi
echo "✅ Security checks passed!\n"

Further, update scripts in package.json to run the audits via the associated package manager.

{
"scripts": {
"audit": "pnpm audit --audit-level=moderate",
"audit:fix": "pnpm audit fix",
"audit:production": "pnpm audit --production --audit-level=high",
"precommit": "pnpm run audit",
"prepush": "pnpm run audit:production"
}
}

4.2 Create Automated Security Reports

This section shows how to create automated security reports for your project.

scripts/generate-security-report.ts
import { execSync } from 'child_process'
import { writeFileSync } from 'fs'
const generateSecurityReport = () => {
console.log('📊 Generating security report...\n')
const report = {
timestamp: new Date().toISOString(),
auditResults: null as any,
outdatedPackages: null as any,
recommendations: [] as string[],
}
try {
// Run npm audit
const auditOutput = execSync('pnpm audit --json', {
encoding: 'utf-8',
})
report.auditResults = JSON.parse(auditOutput)
} catch (error: any) {
// Audit exits with non-zero if vulnerabilities found
if (error.stdout) {
report.auditResults = JSON.parse(error.stdout)
}
}
// Generate recommendations
const vulnerabilities = report.auditResults?.metadata?.vulnerabilities
if (vulnerabilities) {
const { critical, high, moderate } = vulnerabilities
if (critical > 0) {
report.recommendations.push(
`🚨 CRITICAL: ${critical} critical vulnerabilities require immediate attention`
)
}
if (high > 0) {
report.recommendations.push(
`⚠️ HIGH: ${high} high-severity vulnerabilities should be addressed soon`
)
}
if (moderate > 0) {
report.recommendations.push(
`ℹ️ MODERATE: ${moderate} moderate vulnerabilities should be reviewed`
)
}
}
// Write report
const reportPath = `security-report-${Date.now()}.json`
writeFileSync(reportPath, JSON.stringify(report, null, 2))
console.log(`✅ Security report generated: ${reportPath}\n`)
// Print summary
console.log('📊 Summary:')
report.recommendations.forEach(rec => console.log(rec))
return report
}
generateSecurityReport()
  • Run a pnpm audit command to scan your dependencies for vulnerabilities and capture the results as JSON.
  • Collect the discovery timestamp, audit results, and recommendations for remediation.
  • Summarize vulnerabilities by severity (critical, high, moderate), and make actionable recommendations if issues are present.
  • Write the resulting security report to a file you can review or share with your team.

By integrating this script into your workflow, you ensure clear and regular visibility into your project’s security posture. This makes it easier to track and fix vulnerabilities effectively, and supports a policy of continuous security monitoring.

Conclusion

The steps we’ve covered here set up a reliable defense: always use exact versions and lock files, automate security checks in your CI/CD pipeline, turn off install scripts during production builds, use OIDC instead of static credentials, and make security reviews a standard part of your git process.

If you have any questions or comments, feel free to reach out to me on Twitter.

Learn More Protecting Astro from Supply Chain Attacks: Part 1 - Understanding Shai-Hulud 2.0 and Immediate Response
Protecting Astro from Supply Chain Attacks: Part 1 - Understanding Shai-Hulud 2.0 and Immediate Response November 30, 2025
Reusing Database Queries in Astro SSG
Reusing Database Queries in Astro SSG November 29, 2025
Using Partytown with Google Tag Manager in Astro: A Step-by-Step Guide
Using Partytown with Google Tag Manager in Astro: A Step-by-Step Guide November 26, 2025