From apify-pack
Secure Apify API tokens, configure proxy access, and protect Actor data. Use when hardening API key management, setting up environment-specific tokens, or auditing Apify security configuration. Trigger: "apify security", "apify secrets", "secure apify token", "apify API key security", "rotate apify token".
npx claudepluginhub flight505/skill-forge --plugin apify-packThis skill is limited to using the following tools:
Security best practices for Apify API tokens, Actor data, proxy credentials, and webhook verification. Apify uses personal API tokens (prefixed `apify_api_`) for all authentication.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Security best practices for Apify API tokens, Actor data, proxy credentials, and webhook verification. Apify uses personal API tokens (prefixed apify_api_) for all authentication.
Apify uses a single API token per user account for full API access. There is no scope-based permission system per token, so token security is critical.
| Token Type | Format | Where to Find |
|---|---|---|
| Personal API token | apify_api_... | Console > Settings > Integrations |
| Proxy password | Alphanumeric | Console > Proxy > Connection settings |
# .env (NEVER commit — must be in .gitignore)
APIFY_TOKEN=apify_api_YOUR_TOKEN_HERE
# .gitignore — mandatory entries
.env
.env.local
.env.*.local
storage/ # Local Apify storage may contain scraped data
// Validate token exists at startup
function requireToken(): string {
const token = process.env.APIFY_TOKEN;
if (!token) {
throw new Error(
'APIFY_TOKEN is required. Get yours at ' +
'https://console.apify.com/account/integrations'
);
}
if (!token.startsWith('apify_api_')) {
console.warn('Warning: APIFY_TOKEN does not have expected prefix');
}
return token;
}
Use separate Apify accounts (or at minimum separate tokens) per environment:
# Development — your personal account
APIFY_TOKEN=apify_api_dev_token
# Staging — shared team account (limited usage)
APIFY_TOKEN=apify_api_staging_token
# Production — production account (separate billing)
APIFY_TOKEN=apify_api_prod_token
Platform secrets management:
# GitHub Actions
gh secret set APIFY_TOKEN --body "apify_api_prod_token"
# Vercel
vercel env add APIFY_TOKEN production
# Google Cloud Secret Manager
echo -n "apify_api_prod_token" | \
gcloud secrets create apify-token --data-file=-
# 1. Generate new token in Console > Settings > Integrations
# (old token remains valid until explicitly revoked)
# 2. Update in all environments
gh secret set APIFY_TOKEN --body "apify_api_NEW_TOKEN"
# 3. Verify new token works
curl -sf -H "Authorization: Bearer $NEW_TOKEN" \
https://api.apify.com/v2/users/me | jq '.data.username'
# 4. Revoke old token in Console
# Settings > Integrations > (regenerate invalidates old token)
Apify webhooks include run data in the POST body. Verify the source:
import crypto from 'crypto';
import { type Request, type Response } from 'express';
// Apify doesn't sign webhooks by default, but you can verify
// by checking that the run ID in the payload actually exists
async function verifyWebhookPayload(
payload: { eventData: { actorRunId: string } },
client: ApifyClient,
): Promise<boolean> {
try {
const run = await client.run(payload.eventData.actorRunId).get();
return run !== null && run !== undefined;
} catch {
return false;
}
}
// Alternatively, use a shared secret in your webhook URL
// https://your-server.com/webhook?secret=YOUR_WEBHOOK_SECRET
function verifyWebhookSecret(req: Request): boolean {
const secret = req.query.secret as string;
if (!secret || !process.env.APIFY_WEBHOOK_SECRET) return false;
return crypto.timingSafeEqual(
Buffer.from(secret),
Buffer.from(process.env.APIFY_WEBHOOK_SECRET),
);
}
// Sanitize sensitive data before pushing to datasets
function sanitizeForDataset(item: Record<string, unknown>): Record<string, unknown> {
const sensitiveFields = ['email', 'phone', 'password', 'ssn', 'creditCard'];
const sanitized = { ...item };
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = '***REDACTED***';
}
}
return sanitized;
}
// Use named datasets with access control
// Only your account can access your datasets by default
// Public datasets require explicit sharing via API
// Never log or expose proxy URLs (they contain credentials)
const proxyConfig = await Actor.createProxyConfiguration({
groups: ['RESIDENTIAL'],
countryCode: 'US',
});
// DO NOT do this:
// console.log(await proxyConfig.newUrl()); // Leaks proxy password!
// Instead, log proxy group info only
console.log(`Using proxy group: ${proxyConfig.groups?.join(', ')}`);
APIFY_TOKEN stored in environment variables (never hardcoded).env and storage/ in .gitignoreIf a token is exposed:
git log --all -p -- '*.env' '*.json' | grep apify_api_| Issue | Detection | Mitigation |
|---|---|---|
| Token in git history | git log -p | grep apify_api_ | Rotate token, use BFG to clean |
| Unauthorized runs | Unexpected runs in Console | Rotate token immediately |
| Proxy password exposed | Credentials in logs | Regenerate proxy password |
| Data breach in dataset | PII in public dataset | Delete dataset, sanitize pipeline |
For production deployment, see apify-prod-checklist.