Build secure voice agents with proper authentication, data protection, and compliance with industry standards.
Security Overview#
┌────────────────────────────────────────────────────────────────┐
│ Security Layers │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Network │ │ Data │ │ Access │ │
│ │ Security │ │ Security │ │ Control │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ TLS 1.3 │ │ Encryption │ │ API Keys │ │
│ │ WSS │ │ PII Masking │ │ RBAC │ │
│ │ Firewall │ │ Retention │ │ Audit Logs │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
Authentication#
API Key Authentication#
type APIKeyAuth struct {
keys map[string]*APIKey
rateLimit *RateLimiter
}
type APIKey struct {
Key string
WorkspaceID string
Permissions []string
RateLimit int
CreatedAt time.Time
ExpiresAt *time.Time
}
func (a *APIKeyAuth) Authenticate(key string) (*APIKey, error) {
// Hash the key for comparison
hashedKey := hashKey(key)
apiKey, ok := a.keys[hashedKey]
if !ok {
return nil, ErrInvalidAPIKey
}
// Check expiration
if apiKey.ExpiresAt != nil && time.Now().After(*apiKey.ExpiresAt) {
return nil, ErrExpiredAPIKey
}
return apiKey, nil
}
func hashKey(key string) string {
h := sha256.New()
h.Write([]byte(key))
return hex.EncodeToString(h.Sum(nil))
}
Webhook Signature Verification#
Webhook subscription deliveries carry X-Webhook-Signature: t=<unix seconds>,v1=<hex>. Verify v1 as an HMAC-SHA256 of "{t}.{raw body}" with your whsec_... secret, compare in constant time, and reject timestamps older than 5 minutes. Full examples: Webhook Subscriptions.
JWT Authentication#
type JWTAuth struct {
secretKey []byte
issuer string
}
type Claims struct {
jwt.StandardClaims
WorkspaceID string `json:"workspace_id"`
Permissions []string `json:"permissions"`
}
func (j *JWTAuth) GenerateToken(workspaceID string, permissions []string) (string, error) {
claims := Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
IssuedAt: time.Now().Unix(),
Issuer: j.issuer,
},
WorkspaceID: workspaceID,
Permissions: permissions,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.secretKey)
}
func (j *JWTAuth) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return j.secretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrInvalidToken
}
Data Protection#
Encryption at Rest#
type EncryptionService struct {
key []byte
}
func (e *EncryptionService) Encrypt(plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(e.key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func (e *EncryptionService) Decrypt(ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(e.key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
PII Detection and Redaction#
type PIIRedactor struct {
patterns map[string]*regexp.Regexp
}
func NewPIIRedactor() *PIIRedactor {
return &PIIRedactor{
patterns: map[string]*regexp.Regexp{
"credit_card": regexp.MustCompile(`\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`),
"ssn": regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`),
"phone": regexp.MustCompile(`\b\d{3}[-.]?\d{3}[-.]?\d{4}\b`),
"email": regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`),
"aadhaar": regexp.MustCompile(`\b\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`), // Indian Aadhaar
"pan": regexp.MustCompile(`\b[A-Z]{5}[0-9]{4}[A-Z]\b`), // Indian PAN
},
}
}
func (r *PIIRedactor) Redact(text string) string {
replacements := map[string]string{
"credit_card": "[CARD_REDACTED]",
"ssn": "[SSN_REDACTED]",
"phone": "[PHONE_REDACTED]",
"email": "[EMAIL_REDACTED]",
"aadhaar": "[AADHAAR_REDACTED]",
"pan": "[PAN_REDACTED]",
}
for name, pattern := range r.patterns {
text = pattern.ReplaceAllString(text, replacements[name])
}
return text
}
func (r *PIIRedactor) DetectPII(text string) []PIIMatch {
var matches []PIIMatch
for name, pattern := range r.patterns {
locs := pattern.FindAllStringIndex(text, -1)
for _, loc := range locs {
matches = append(matches, PIIMatch{
Type: name,
Start: loc[0],
End: loc[1],
Value: text[loc[0]:loc[1]],
})
}
}
return matches
}
Secure Recording#
type SecureRecorder struct {
encryption *EncryptionService
storage Storage
piiRedactor *PIIRedactor
}
func (r *SecureRecorder) SaveRecording(callID string, audio []byte, transcript string) error {
// Encrypt audio
encryptedAudio, err := r.encryption.Encrypt(audio)
if err != nil {
return err
}
// Redact PII from transcript
redactedTranscript := r.piiRedactor.Redact(transcript)
// Store with encryption key reference
return r.storage.Save(Recording{
CallID: callID,
EncryptedAudio: encryptedAudio,
RedactedTranscript: redactedTranscript,
EncryptionKeyID: r.encryption.KeyID(),
CreatedAt: time.Now(),
})
}
Access Control#
Role-Based Access Control (RBAC)#
type Permission string
const (
PermissionAgentRead Permission = "agent:read"
PermissionAgentWrite Permission = "agent:write"
PermissionCallRead Permission = "call:read"
PermissionCallWrite Permission = "call:write"
PermissionRecordRead Permission = "recording:read"
PermissionRecordDelete Permission = "recording:delete"
PermissionAdmin Permission = "admin"
)
type Role struct {
Name string
Permissions []Permission
}
var Roles = map[string]Role{
"admin": {
Name: "admin",
Permissions: []Permission{
PermissionAgentRead, PermissionAgentWrite,
PermissionCallRead, PermissionCallWrite,
PermissionRecordRead, PermissionRecordDelete,
PermissionAdmin,
},
},
"operator": {
Name: "operator",
Permissions: []Permission{
PermissionAgentRead,
PermissionCallRead, PermissionCallWrite,
PermissionRecordRead,
},
},
"viewer": {
Name: "viewer",
Permissions: []Permission{
PermissionAgentRead,
PermissionCallRead,
},
},
}
type RBAC struct {
userRoles map[string][]string
}
func (r *RBAC) HasPermission(userID string, permission Permission) bool {
roles := r.userRoles[userID]
for _, roleName := range roles {
role := Roles[roleName]
for _, p := range role.Permissions {
if p == permission || p == PermissionAdmin {
return true
}
}
}
return false
}
Request Authorization#
func AuthorizationMiddleware(rbac *RBAC, permission Permission) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("user_id").(string)
if !rbac.HasPermission(userID, permission) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// Usage
router.Handle("/agents", AuthorizationMiddleware(rbac, PermissionAgentRead)(agentHandler))
Audit Logging#
Audit Events#
type AuditEvent struct {
ID string
Timestamp time.Time
UserID string
Action string
Resource string
ResourceID string
Details map[string]any
IPAddress string
UserAgent string
Success bool
ErrorReason string
}
type AuditLogger struct {
storage Storage
}
func (l *AuditLogger) Log(ctx context.Context, event AuditEvent) {
event.ID = uuid.New().String()
event.Timestamp = time.Now()
event.IPAddress = GetIPFromContext(ctx)
event.UserAgent = GetUserAgentFromContext(ctx)
// Async write to avoid blocking
go func() {
if err := l.storage.Save(event); err != nil {
log.Printf("Failed to save audit log: %v", err)
}
}()
}
// Usage
auditLogger.Log(ctx, AuditEvent{
UserID: userID,
Action: "agent.update",
Resource: "agent",
ResourceID: agentID,
Details: map[string]any{
"changed_fields": []string{"prompt", "greeting"},
},
Success: true,
})
Sensitive Data Logging#
func (l *AuditLogger) LogSensitiveAccess(ctx context.Context, dataType, resourceID string) {
l.Log(ctx, AuditEvent{
UserID: GetUserID(ctx),
Action: "sensitive_data.access",
Resource: dataType,
ResourceID: resourceID,
Details: map[string]any{
"purpose": "customer_support",
},
Success: true,
})
}
Network Security#
TLS Configuration#
func SecureTLSConfig() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
}
}
func StartSecureServer(addr string, handler http.Handler) error {
server := &http.Server{
Addr: addr,
Handler: handler,
TLSConfig: SecureTLSConfig(),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
return server.ListenAndServeTLS("cert.pem", "key.pem")
}
Rate Limiting#
type RateLimiter struct {
limits map[string]*rate.Limiter
mu sync.RWMutex
}
func (r *RateLimiter) Allow(key string, limit rate.Limit, burst int) bool {
r.mu.Lock()
defer r.mu.Unlock()
limiter, ok := r.limits[key]
if !ok {
limiter = rate.NewLimiter(limit, burst)
r.limits[key] = limiter
}
return limiter.Allow()
}
func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if !limiter.Allow(key, 100, 10) { // 100 req/sec, burst 10
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
Compliance#
Edesy does not claim GDPR, PCI-DSS or SOC 2 certification. The code below shows controls you can build in your own systems; take payments through payment links (Razorpay, Stripe, PayU) rather than collecting card numbers on a call.
GDPR Compliance#
type GDPRHandler struct {
storage Storage
}
// Right to be forgotten
func (h *GDPRHandler) DeleteUserData(userID string) error {
// Delete all user-related data
if err := h.storage.DeleteTranscripts(userID); err != nil {
return err
}
if err := h.storage.DeleteRecordings(userID); err != nil {
return err
}
if err := h.storage.DeleteCallLogs(userID); err != nil {
return err
}
return nil
}
// Right to data portability
func (h *GDPRHandler) ExportUserData(userID string) (*UserDataExport, error) {
transcripts, _ := h.storage.GetTranscripts(userID)
callLogs, _ := h.storage.GetCallLogs(userID)
return &UserDataExport{
UserID: userID,
Transcripts: transcripts,
CallLogs: callLogs,
ExportedAt: time.Now(),
}, nil
}
// Data retention
func (h *GDPRHandler) ApplyRetentionPolicy(maxAgeDays int) error {
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
return h.storage.DeleteOlderThan(cutoff)
}
PCI-DSS Compliance#
type PCICompliantDTMF struct {
// Never log DTMF during sensitive input
sensitiveMode bool
recording *Recorder
}
func (d *PCICompliantDTMF) EnterSensitiveMode() {
d.sensitiveMode = true
d.recording.Pause() // Stop recording
}
func (d *PCICompliantDTMF) ExitSensitiveMode() {
d.sensitiveMode = false
d.recording.Resume()
}
func (d *PCICompliantDTMF) CollectCardNumber() string {
d.EnterSensitiveMode()
defer d.ExitSensitiveMode()
// Collect DTMF without logging
digits := d.collectDTMF(16)
// Tokenize immediately
token := d.tokenizer.Tokenize(digits)
// Never store raw card number
return token
}
Security Checklist#
API Security#
- Use API keys with rotation policy
- Implement rate limiting
- Validate and sanitize all input
- Use HTTPS/WSS only
- Implement request signing
Data Security#
- Encrypt data at rest
- Encrypt data in transit (TLS 1.2+)
- Redact PII from logs and transcripts
- Implement data retention policies
- Secure key management
Access Control#
- Implement RBAC
- Audit all access
- Use principle of least privilege
- Regular access reviews
Compliance (your systems)#
- Data export and deletion process for your callers
- Card payments through payment links, not spoken card numbers
- Your own security reviews of webhook endpoints and stored transcripts
Next Steps#
- Error Handling - Resilience patterns
- Monitoring - Security monitoring
- Scaling - Secure scaling