Security

Implement security best practices for voice agents including authentication, data protection, and compliance.

2 min read

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#

Was this page helpful?