Security Best Practices
Note: Code examples in this guide are illustrative pseudocode showing recommended patterns. For working examples using the actual LLM4S API, see modules/samples .
Comprehensive security patterns for protecting sensitive data, managing credentials, and maintaining audit trails in production LLM systems.
API Key Management
Note: Code examples in this guide are illustrative pseudocode showing recommended patterns. For working examples using the actual LLM4S API, see modules/samples .
Configuration-Based Access
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object SecureKeyManagement {
import org.llm4s.config.Llm4sConfig
def loadApiKey(provider: String): Result[String] = {
Llm4sConfig.provider(provider).apiKey match {
case Some(key) if key.nonEmpty => Result.success(key)
case _ => Result.failure(
s"API key for provider '$provider' not configured. " +
s"Set it in your configuration file or environment."
)
}
}
// Safe logging (don't log keys!)
def logApiUsage(key: String, provider: String): Unit = {
val maskedKey = key.take(5) + "..." + key.takeRight(5)
println(s"Using provider: $provider with key: $maskedKey")
}
}
Key Vault Integration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
object KeyVault {
import com.azure.security.keyvault.secrets._
class SecureKeyManager(vaultUri: String) {
private val client = new SecretClientBuilder()
.vaultUrl(vaultUri)
.buildClient()
def getKey(keyName: String): Result[String] = {
try {
val secret = client.getSecret(keyName)
Result.success(secret.getValue)
} catch {
case e: Exception =>
Result.failure(s"Failed to retrieve key from vault: ${e.getMessage}")
}
}
}
// Usage
def getOpenAIKey(): Result[String] = {
val vaultManager = new SecureKeyManager("https://myvault.vault.azure.net/")
vaultManager.getKey("openai-api-key")
}
}
Key Rotation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
object KeyRotation {
case class KeyMetadata(
key: String,
provider: String,
createdAt: Long,
rotatedAt: Long,
expiresAt: Long
) {
def isExpired: Boolean = System.currentTimeMillis() > expiresAt
def daysSinceRotation: Long =
(System.currentTimeMillis() - rotatedAt) / (1000 * 60 * 60 * 24)
}
class KeyRotationManager {
private var activeKey: KeyMetadata = _
private var rotationScheduler: Option[java.util.Timer] = None
def rotateKey(newKey: String, provider: String): Unit = {
val now = System.currentTimeMillis()
val thirtyDaysFromNow = now + (30 * 24 * 60 * 60 * 1000)
activeKey = KeyMetadata(
key = newKey,
provider = provider,
createdAt = now,
rotatedAt = now,
expiresAt = thirtyDaysFromNow
)
println(s"Key rotated for $provider. Valid until ${new java.util.Date(thirtyDaysFromNow)}")
}
def scheduleRotation(intervalDays: Int = 30): Unit = {
val timer = new java.util.Timer()
val delayMs = intervalDays * 24 * 60 * 60 * 1000L
timer.scheduleAtFixedRate(
new java.util.TimerTask {
def run(): Unit = {
println("Time to rotate API keys!")
// In production: fetch new key from vault and rotate
}
},
delayMs,
delayMs
)
rotationScheduler = Some(timer)
}
}
}
Query Validation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
object InputValidation {
case class ValidationRule(
name: String,
validate: String => Boolean,
errorMessage: String
)
class InputValidator {
private val rules = scala.collection.mutable.ListBuffer[ValidationRule]()
def addRule(rule: ValidationRule): Unit = {
rules += rule
}
def validate(input: String): Result[String] = {
rules.find(!_.validate(input)) match {
case Some(failedRule) =>
Result.failure(failedRule.errorMessage)
case None =>
Result.success(input)
}
}
}
// Common validation rules
def setupCommonValidations(): InputValidator = {
val validator = new InputValidator()
// Length check
validator.addRule(ValidationRule(
name = "length",
validate = _.length <= 10000,
errorMessage = "Input exceeds maximum length of 10000 characters"
))
// No injection patterns
validator.addRule(ValidationRule(
name = "no_injection",
validate = !containsSQLInjectionPattern(_),
errorMessage = "Input contains potentially harmful patterns"
))
// No excessive special characters
validator.addRule(ValidationRule(
name = "no_spam",
validate = countSpecialChars(_) < 0.5,
errorMessage = "Input contains too many special characters"
))
validator
}
private def containsSQLInjectionPattern(input: String): Boolean = {
val sqlPatterns = List("';", "--", "/*", "*/", "xp_", "sp_")
sqlPatterns.exists(input.toLowerCase.contains(_))
}
private def countSpecialChars(input: String): Double = {
val specialCount = input.count(!_.isLetterOrDigit)
specialCount.toDouble / input.length
}
}
Prompt Injection Protection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
object PromptInjectionProtection {
def detectPromptInjection(userInput: String): Boolean = {
val injectionIndicators = List(
"ignore all previous instructions",
"forget everything",
"system prompt",
"you are now",
"instead please",
"role play as",
"pretend you are"
)
val lowerInput = userInput.toLowerCase
injectionIndicators.exists(lowerInput.contains(_))
}
def sanitizePrompt(userInput: String): Result[String] = {
if (detectPromptInjection(userInput)) {
Result.failure(
"Your input was flagged as potentially malicious. " +
"Please rephrase your request without system instructions."
)
} else {
Result.success(userInput)
}
}
// Use prompt templating to separate user input
def safePromptTemplate(
userQuery: String,
systemPrompt: String
): String = {
s"""$systemPrompt
User Query:
[BEGIN USER INPUT]
$userQuery
[END USER INPUT]
Please answer based only on the user's question above."""
}
}
Output Sanitization
PII Redaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
object PIIRedaction {
import scala.util.matching.Regex
object PatternDetectors {
val emailPattern = new Regex(
"""[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"""
)
val phonePattern = new Regex(
"""(\d{3}[-.\s]?){2}\d{4}"""
)
val ssn Pattern = new Regex(
"""\b\d{3}-\d{2}-\d{4}\b"""
)
val creditCardPattern = new Regex(
"""(?:\d[ -]*?){13,16}"""
)
}
class PIIRedactor {
def redact(text: String): String = {
var result = text
// Redact emails
result = PatternDetectors.emailPattern.replaceAllIn(
result,
"[EMAIL]"
)
// Redact phone numbers
result = PatternDetectors.phonePattern.replaceAllIn(
result,
"[PHONE]"
)
// Redact SSNs
result = PatternDetectors.ssnPattern.replaceAllIn(
result,
"[SSN]"
)
// Redact credit cards
result = PatternDetectors.creditCardPattern.replaceAllIn(
result,
"[CREDIT_CARD]"
)
result
}
}
// Usage
def answerWithSanitization(
query: String,
agent: Agent
): Result[String] = {
agent.run(query).map { response =>
val redactor = new PIIRedactor()
redactor.redact(response.message)
}
}
}
Audit Logging
Comprehensive Audit Trail
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
object AuditLogging {
import org.slf4j.LoggerFactory
import java.time.Instant
val auditLogger = LoggerFactory.getLogger("AUDIT")
case class AuditEvent(
timestamp: Instant,
userId: String,
action: String,
resource: String,
details: Map[String, String],
success: Boolean,
errorMessage: Option[String] = None
)
def logAPICall(
userId: String,
provider: String,
model: String,
inputTokens: Int,
outputTokens: Int
): Unit = {
val event = AuditEvent(
timestamp = Instant.now(),
userId = userId,
action = "API_CALL",
resource = s"$provider/$model",
details = Map(
"input_tokens" -> inputTokens.toString,
"output_tokens" -> outputTokens.toString
),
success = true
)
auditLogger.info(
s"${event.timestamp} | User: ${event.userId} | " +
s"Action: ${event.action} | Resource: ${event.resource}"
)
}
def logAccessToSensitiveData(
userId: String,
dataType: String,
recordCount: Int,
accessReason: String
): Unit = {
auditLogger.info(
s"${Instant.now()} | User: $userId | " +
s"Accessed: $dataType ($recordCount records) | " +
s"Reason: $accessReason"
)
}
def logAuthenticationAttempt(
userId: String,
success: Boolean,
method: String
): Unit = {
auditLogger.info(
s"${Instant.now()} | User: $userId | " +
s"Authentication: ${if (success) "SUCCESS" else "FAILED"} via $method"
)
}
}
Compliance Logging
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
object ComplianceLogging {
case class DataProcessingLog(
timestamp: Long,
userId: String,
dataCategory: String,
operation: String, // PROCESS, STORE, SHARE, DELETE
retentionDays: Int,
userConsent: Boolean
)
class ComplianceLogger {
private val logs = scala.collection.mutable.ListBuffer[DataProcessingLog]()
def logDataProcessing(
userId: String,
dataCategory: String,
operation: String,
retentionDays: Int
): Unit = {
logs += DataProcessingLog(
timestamp = System.currentTimeMillis(),
userId = userId,
dataCategory = dataCategory,
operation = operation,
retentionDays = retentionDays,
userConsent = true
)
}
def getComplianceReport(days: Int = 30): String = {
val cutoff = System.currentTimeMillis() - (days * 24 * 60 * 60 * 1000L)
val recent = logs.filter(_.timestamp > cutoff)
val operationCounts = recent.groupBy(_.operation).mapValues(_.length)
val dataCategoryCounts = recent.groupBy(_.dataCategory).mapValues(_.length)
s"""Compliance Report (last $days days):
|Operations: $operationCounts
|Data Categories: $dataCategoryCounts
|Total Records Processed: ${recent.length}""".stripMargin
}
}
}
Data Retention
Automatic Data Expiration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
object DataRetention {
case class DataRecord(
id: String,
data: String,
createdAt: Long,
ttlDays: Int
) {
def isExpired: Boolean = {
val now = System.currentTimeMillis()
val expirationTime = createdAt + (ttlDays * 24 * 60 * 60 * 1000L)
now > expirationTime
}
}
class RetentionPolicy {
def getRetentionDays(dataType: String): Int = dataType match {
case "user_query" => 30
case "api_key" => 365
case "error_log" => 90
case "audit_log" => 365 * 7 // 7 years
case _ => 30
}
def scheduleCleanup(): Unit = {
val timer = new java.util.Timer()
val dailyMs = 24 * 60 * 60 * 1000L
timer.scheduleAtFixedRate(
new java.util.TimerTask {
def run(): Unit = {
println("Running data retention cleanup...")
// Delete expired records from database
}
},
dailyMs,
dailyMs
)
}
}
}
Best Practices
✅ Do’s
Never log API keys : Even in debug mode
Rotate keys regularly : At least every 30 days
Use key vaults : Don’t hardcode secrets
Validate all input : Both user queries and system inputs
Sanitize all output : Remove PII before returning
Keep audit trails : For compliance and debugging
Use HTTPS only : For all API communication
Implement rate limiting : To prevent abuse
Monitor suspicious patterns : Unusual access or usage
Use least privilege : Grant minimal required permissions
❌ Don’ts
Don’t store keys in code : Use environment variables or vaults
Don’t trust user input : Always validate and sanitize
Don’t output sensitive data : Check for PII before returning
Don’t log full queries : Log only hashed or redacted version
Don’t skip authentication : Verify user identity
Don’t use weak passwords : Enforce strong credential policies
Don’t ignore security updates : Keep dependencies patched
Don’t expose error details : Log full errors, show generic messages to users
Don’t test in production : Use staging for security tests
Don’t assume perimeter security : Defense in depth required
Security Checklist
✅ Before Production
See Also
Last Updated: February 2026
Status: Production Ready