Security

Arctickey is designed with security as a priority. Here's how we protect your data and how you can enhance security further.

Built-In Security#

TLS Encryption#

All connections to Arctickey use TLS 1.3 by default:

  • In-transit encryption: Data is encrypted between your app and Arctickey
  • Modern cipher suites: We use only secure, modern ciphers
  • Certificate validation: Our certificates are signed by trusted CAs
TypeScript
// TLS is automatic with rediss:// URLs import Redis from 'ioredis'; const redis = new Redis(process.env.ARCTICKEY_URL); // URL format: rediss://default:password@host:port // Or explicitly configure TLS const redis = new Redis({ host: 'your-db.arctickey.com', port: 6379, password: 'your-password', tls: { // TLS options (usually not needed) }, });

Password Authentication#

Every database has a unique, strong password:

  • Auto-generated: 32+ character random passwords
  • Rotatable: Generate new passwords anytime
  • Per-database: Each database has its own credentials

Network Isolation#

  • Dedicated instances: Your data is isolated from other customers
  • Private networking: Available on Growth plans and above
  • IP allowlisting: Restrict connections to specific IPs (coming soon)

Best Practices#

Secure Your Connection String#

TypeScript
// ✅ Good: Use environment variables const redis = new Redis(process.env.ARCTICKEY_URL); // ❌ Bad: Hardcoded credentials const redis = new Redis('rediss://default:secret@host:6379');

Store credentials securely:

  • Use environment variables
  • Use secrets managers (AWS Secrets Manager, HashiCorp Vault)
  • Never commit credentials to git

Rotate Credentials Regularly#

  1. Go to your database in the Arctickey dashboard
  2. Click "Security" → "Regenerate Password"
  3. Update your application with the new connection string
  4. Deploy your changes

Use Minimal Permissions#

If you have multiple services accessing Redis:

  • Create separate databases for different environments (dev, staging, prod)
  • Consider separate databases for different services
  • Use different credentials per service when possible

Validate Input#

Never use user input directly in Redis keys:

TypeScript
// ❌ Bad: User input in key const key = `user:${req.params.id}`; // Could be "user:*" or "user:../admin" await redis.get(key); // ✅ Good: Validate and sanitize const userId = parseInt(req.params.id); if (!Number.isInteger(userId) || userId < 0) { throw new Error('Invalid user ID'); } const key = `user:${userId}`; await redis.get(key);

Set Appropriate TTLs#

Don't store sensitive data indefinitely:

TypeScript
// Session data - expire after inactivity await redis.setex(`session:${sessionId}`, 3600, JSON.stringify(session)); // Sensitive tokens - short TTL await redis.setex(`reset:${token}`, 600, userId); // 10 minutes // Refresh on access (sliding expiration) await redis.expire(`session:${sessionId}`, 3600);

Authentication Patterns#

Session Storage#

TypeScript
import crypto from 'crypto'; class SessionStore { private ttl = 24 * 60 * 60; // 24 hours async createSession(userId: string, data: object) { const sessionId = crypto.randomBytes(32).toString('hex'); await redis.setex( `session:${sessionId}`, this.ttl, JSON.stringify({ userId, ...data, createdAt: Date.now() }) ); return sessionId; } async getSession(sessionId: string) { // Validate session ID format if (!/^[a-f0-9]{64}$/.test(sessionId)) { return null; } const data = await redis.get(`session:${sessionId}`); if (!data) return null; // Refresh TTL on access await redis.expire(`session:${sessionId}`, this.ttl); return JSON.parse(data); } async destroySession(sessionId: string) { await redis.del(`session:${sessionId}`); } async destroyAllUserSessions(userId: string) { // Track user's sessions in a set const sessions = await redis.smembers(`user:${userId}:sessions`); if (sessions.length > 0) { await redis.del(...sessions.map(s => `session:${s}`)); await redis.del(`user:${userId}:sessions`); } } }

JWT Refresh Token Storage#

TypeScript
class TokenStore { // Store refresh token with user association async storeRefreshToken(token: string, userId: string, ttlDays: number = 30) { const key = `refresh:${token}`; const ttl = ttlDays * 24 * 60 * 60; await redis.pipeline() .setex(key, ttl, userId) .sadd(`user:${userId}:tokens`, token) .expire(`user:${userId}:tokens`, ttl) .exec(); } // Validate and get user ID async validateRefreshToken(token: string): Promise<string | null> { return redis.get(`refresh:${token}`); } // Revoke a specific token async revokeToken(token: string) { const userId = await redis.get(`refresh:${token}`); if (userId) { await redis.pipeline() .del(`refresh:${token}`) .srem(`user:${userId}:tokens`, token) .exec(); } } // Revoke all tokens for a user (logout everywhere) async revokeAllTokens(userId: string) { const tokens = await redis.smembers(`user:${userId}:tokens`); if (tokens.length > 0) { const pipeline = redis.pipeline(); tokens.forEach(t => pipeline.del(`refresh:${t}`)); pipeline.del(`user:${userId}:tokens`); await pipeline.exec(); } } }

Rate Limiting for Auth#

TypeScript
class AuthRateLimiter { // Limit login attempts per IP async checkLoginAttempt(ip: string): Promise<{ allowed: boolean; remaining: number }> { const key = `ratelimit:login:${ip}`; const limit = 5; const window = 15 * 60; // 15 minutes const current = await redis.incr(key); if (current === 1) { await redis.expire(key, window); } return { allowed: current <= limit, remaining: Math.max(0, limit - current), }; } // Lock account after too many failures async recordFailedLogin(userId: string): Promise<boolean> { const key = `failed:login:${userId}`; const lockThreshold = 10; const window = 60 * 60; // 1 hour const failures = await redis.incr(key); if (failures === 1) { await redis.expire(key, window); } if (failures >= lockThreshold) { // Lock the account await redis.setex(`locked:${userId}`, window, '1'); return true; // Account is now locked } return false; } async isAccountLocked(userId: string): Promise<boolean> { return await redis.exists(`locked:${userId}`) === 1; } async clearFailedAttempts(userId: string) { await redis.del(`failed:login:${userId}`); } }

Compliance#

GDPR#

Arctickey helps you comply with GDPR:

  • Data location: All data stored in EU (Germany)
  • Data sovereignty: Swedish company, EU jurisdiction
  • Right to erasure: Delete user data with DEL commands
  • Data portability: Export with DUMP/RESTORE or application logic

Data Retention#

Implement data retention policies:

TypeScript
// Set TTL on all user data await redis.setex(`user:${userId}`, 365 * 24 * 60 * 60, userData); // Or use EXPIRE on existing keys await redis.expire(`user:${userId}`, 365 * 24 * 60 * 60); // Scan and clean old data async function cleanOldData(pattern: string, maxAge: number) { let cursor = '0'; do { const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); cursor = nextCursor; for (const key of keys) { const ttl = await redis.ttl(key); if (ttl === -1) { // No expiry set await redis.expire(key, maxAge); } } } while (cursor !== '0'); }

Audit Logging#

Track sensitive operations:

TypeScript
class AuditLog { async log(event: { action: string; userId?: string; resource: string; details?: object; ip?: string; }) { const entry = { ...event, timestamp: new Date().toISOString(), }; // Store in a stream for durability await redis.xadd( 'audit:log', 'MAXLEN', '~', 100000, // Keep ~100k entries '*', 'data', JSON.stringify(entry) ); } async getRecentLogs(count: number = 100) { const entries = await redis.xrevrange('audit:log', '+', '-', 'COUNT', count); return entries.map(([id, fields]) => ({ id, ...JSON.parse(fields[1]), })); } } // Usage await auditLog.log({ action: 'user.login', userId: user.id, resource: 'auth', ip: req.ip, });

Security Checklist#

  • Using rediss:// (TLS) connection URL
  • Credentials stored in environment variables
  • No credentials in source code or git
  • Input validation on all user-provided keys
  • TTLs set on sensitive data
  • Rate limiting on authentication endpoints
  • Session invalidation on logout
  • Regular credential rotation
  • Audit logging for sensitive operations