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 KitSvelteKit Starter KitAstro 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:
# Always use exact versions when saving packagessave-exact=true
# Ensure package-lock.json is always created and usedpackage-lock=true
# Run audit on every installaudit=true
# Set minimum audit level (low, moderate, high, critical)audit-level=moderate
# Fail install if vulnerabilities are foundaudit-report=trueFor pnpm users (recommended), configure additional security features:
# File: .npmrc (for pnpm)
# Require exact versionssave-exact=true
# Strict peer dependenciesstrict-peer-dependencies=true
# Verify package integrity using checksumspackage-import-method=copy
# Run security audit on installaudit=true
# Only allow packages from verified registriesregistry=https://registry.npmjs.org/
# Verify SSL certificatesstrict-ssl=true1.3 Always Commit Lock Files
Your lock file (package-lock.json or pnpm-lock.yaml) is critical for reproducible builds:
# Ensure lock file is tracked in gitgit add pnpm-lock.yamlgit commit -m "chore: commit lock file for security"Update your .gitignore to ensure lock files are never ignored:
# Make sure these are NOT in .gitignore:# pnpm-lock.yaml# package-lock.json# yarn.lock
# But DO ignorenode_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:
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: 90Snyk 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:
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:
# Install Snyk CLI globallynpm install -g snyk
# Authenticate with Snyksnyk auth
# Scan your projectsnyk test
# Monitor your project (sends snapshot to Snyk for continuous monitoring)snyk monitorAdd a pre-push git hook:
# Install husky if you haven't alreadypnpm add -D husky
# Initialize huskynpx husky init
# Create pre-push hookcat > .husky/pre-push << 'EOF'#!/bin/sh. "$(dirname "$0")/_/husky.sh"
echo "🔍 Running security scan before push..."pnpm audit --audit-level=moderateEOF
chmod +x .husky/pre-push3. 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:
name: Deploy to Production
on: push: branches: [main]
# Set minimal default permissionspermissions: 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 deploy3.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-scriptsFor packages that legitimately need install scripts (like sharp or esbuild), rebuild them explicitly:
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.
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:stagingname: 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:production3.4 Use OIDC Instead of Long-Lived Tokens
For cloud deployments, use OpenID Connect (OIDC) instead of storing long-lived credentials:
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.
#!/bin/sh. "$(dirname "$0")/_/husky.sh"
echo "🔍 Running pre-commit security checks...\n"
# Run auditecho "📦 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 1fi
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.
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 auditcommand 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.