Skip to content
StackPractices
intermediate Por StackPractices

Privacidad de Datos y Cumplimiento GDPR

Implementa controles de privacidad de datos, gestión de consentimientos, anonimización y manejo GDPR-compliant en aplicaciones web.

Temas: security

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

El Reglamento General de Protección de Datos (GDPR) establece reglas estrictas para el manejo de datos personales de ciudadanos de la UE. Más allá del cumplimiento legal, el privacy-by-design construye confianza del usuario y reduce el impacto de brechas. Este recurso cubre controles técnicos prácticos: gestión de consentimientos, minimización de datos, anonimización, derecho al olvido y portabilidad de datos.

Cuándo Usar

Usa este recurso cuando:

  • Procesas datos personales de residentes de la UE (clientes, empleados, leads)
  • Construyes flujos de consentimiento para marketing, analytics o sharing con terceros
  • Implementas políticas de retención de datos y eliminación automática
  • Te preparas para data subject access requests (DSARs)

Solución

Esquema de Gestión de Consentimientos (PostgreSQL)

CREATE TABLE user_consents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    purpose VARCHAR(50) NOT NULL,  -- 'marketing', 'analytics', 'third_party'
    granted BOOLEAN NOT NULL DEFAULT false,
    granted_at TIMESTAMPTZ,
    withdrawn_at TIMESTAMPTZ,
    ip_address INET,
    user_agent TEXT
);

-- Índice para búsquedas DSAR rápidas
CREATE INDEX idx_consents_user ON user_consents(user_id);

Pipeline de Anonimización (Python)

import hashlib
import hmac
from datetime import datetime

SECRET_KEY = b"rotation-secret-2024"

def pseudonymize(user_id: str) -> str:
    """Reemplaza PII con hash irreversible para analytics."""
    return hmac.new(SECRET_KEY, user_id.encode(), hashlib.sha256).hexdigest()[:16]

def anonymize_age(birth_date: datetime) -> str:
    """Agrupa edad en rangos para uso estadístico."""
    age = (datetime.now() - birth_date).days // 365
    if age < 18: return "<18"
    if age < 25: return "18-24"
    if age < 35: return "25-34"
    return "35+"

Derecho al Olvido (Node.js)

async function eraseUserData(userId) {
  // 1. Eliminar o anonimizar registro de usuario
  await db.users.update(userId, {
    email: null,
    name: 'Usuario Eliminado',
    deleted_at: new Date()
  });
  
  // 2. Eliminar datos relacionados en cascada
  await db.orders.anonymize({ user_id: userId });
  await db.sessions.delete({ user_id: userId });
  await db.activity_logs.delete({ user_id: userId });
  
  // 3. Notificar a terceros
  await webhookService.notify('user.deleted', { userId });
  
  // 4. Loggear la eliminación para auditoría
  await auditLog.create({ action: 'erasure', user_id: userId, timestamp: new Date() });
}

Explicación

Seis principios del GDPR con implementación técnica:

  1. Legalidad: Almacena registros de consentimiento con timestamp, IP y propósito
  2. Limitación de propósito: Etiqueta datos con propósitos permitidos; rechaza uso no autorizado
  3. Minimización de datos: Recolecta solo lo necesario; usa validación de formularios
  4. Exactitud: Permite a usuarios actualizar perfiles; valida inputs
  5. Limitación de almacenamiento: TTL en datos personales; auto-eliminación después del período de retención
  6. Integridad/confidencialidad: Encripta en reposo; hashea para analytics; controles de acceso

Derechos del Sujeto de Datos (DSRs):

  • Acceso: Exportar todos los datos personales en formato legible por máquina (JSON)
  • Rectificación: Actualizar datos incorrectos vía portal self-service
  • Olvido: Eliminar o anonimizar dentro de 30 días (con audit trail)
  • Portabilidad: Exportar datos en formato estándar (JSON, CSV)
  • Objeción: Detener procesamiento para marketing o profiling

Variantes

ControlImplementaciónEsfuerzo
Banners de consentimientoCookie consent SDKBajo
Data mappingAuditoría manual + tags de schemaMedio
Auto-eliminaciónCron jobs con reglas de retenciónMedio
PseudonimizaciónHashing + rotación de clavesMedio
Dashboard DPOHerramienta admin customAlto

Lo que funciona

  • Privacy by default: Nuevas capacidades opt-out por defecto; requiere opt-in explícito
  • Encripta PII en reposo: Usa AES-256 para bases de datos, S3 y backups
  • Loguea acceso a datos personales: Audita quién accedió qué y cuándo
  • Separa datos de analytics: Pseudonimiza antes de enviar a herramientas BI
  • Documenta tu base legal: Contrato, consentimiento o interés legítimo por tipo de dato

Errores Comunes

  1. Consentimiento implícito: Checkboxes pre-marcadas o términos escondidos no cuentan bajo GDPR
  2. Sin límites de retención: Mantener datos de usuario para siempre viola limitación de almacenamiento
  3. Filtración a terceros: Analytics, ads y CDNs reciben PII sin conocimiento del usuario. Ejecuta auditorías de dependencias regularmente.
  4. Ignorar eliminación de backups: Los datos de usuario borrados persisten en snapshots antiguos de base de datos
  5. Consentimiento único para todo: Las cookies de marketing y funcionales necesitan consentimiento separado

Preguntas Frecuentes

P: ¿Aplica el GDPR si mi empresa está fuera de la UE? R: Sí, si procesas datos de residentes de la UE. El reglamento tiene alcance extraterritorial.

P: ¿Cuál es la diferencia entre anonimización y pseudonimización? R: La anonimización es irreversible (los datos nunca pueden identificar a la persona). La pseudonimización usa una clave que puede re-identificar si es necesario (sigue siendo datos personales bajo GDPR).

P: ¿Cómo manejo solicitudes de olvido con foreign key constraints? R: Usa soft deletes (anonimiza en lugar de eliminar), o eliminación en cascada con audit logging. Nunca dejes registros huérfanos.

¿Esta solución está lista para producción?

Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.

¿Cuáles son las características de rendimiento?

El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.

¿Cómo depuro problemas con este enfoque?

Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.

Soluciones Avanzadas

Exportación de portabilidad de datos (Python)

El Artículo 20 del GDPR requiere exportar datos de usuario en formato legible por máquina:

import json
from datetime import datetime
from io import BytesIO
from zipfile import ZipFile

def export_user_data(user_id: str, db) -> bytes:
    """Exportar todos los datos personales de un usuario como ZIP con archivos JSON."""
    data = {
        'profile': db.get_user_profile(user_id),
        'orders': db.get_user_orders(user_id),
        'consents': db.get_user_consents(user_id),
        'activity_logs': db.get_user_activity(user_id),
    }

    # Remover campos internos y mantener solo datos personales
    for section in data.values():
        if isinstance(section, list):
            for item in section:
                item.pop('id', None)
                item.pop('internal_status', None)
        elif isinstance(section, dict):
            section.pop('id', None)

    export = {
        'user_id': user_id,
        'exported_at': datetime.utcnow().isoformat() + 'Z',
        'format': 'GDPR Article 20 Data Portability',
        'data': data,
    }

    json_bytes = json.dumps(export, indent=2, default=str).encode('utf-8')

    # Empaquetar como ZIP para descarga
    buffer = BytesIO()
    with ZipFile(buffer, 'w') as zf:
        zf.writestr('user_data.json', json_bytes)

    return buffer.getvalue()

# Uso en Flask
@app.route('/api/gdpr/export', methods=['POST'])
@require_auth
def request_export():
    user_id = request.user.id
    data = export_user_data(user_id, db)
    return send_file(
        BytesIO(data),
        mimetype='application/zip',
        as_attachment=True,
        download_name=f'gdpr_export_{user_id}.zip'
    )

API de gestión de consentimientos (Node.js)

const express = require('express');
const router = express.Router();

// Otorgar consentimiento para un propósito específico
router.post('/consent/:purpose', async (req, res) => {
  const { purpose } = req.params;
  const userId = req.user.id;
  const allowedPurposes = ['marketing', 'analytics', 'third_party', 'cookies_functional'];

  if (!allowedPurposes.includes(purpose)) {
    return res.status(400).json({ error: 'Propósito de consentimiento inválido' });
  }

  const consent = await db.consents.upsert({
    user_id: userId,
    purpose,
    granted: true,
    granted_at: new Date(),
    withdrawn_at: null,
    ip_address: req.ip,
    user_agent: req.headers['user-agent'],
  });

  res.json({ purpose, granted: true, granted_at: consent.granted_at });
});

// Retirar consentimiento
router.delete('/consent/:purpose', async (req, res) => {
  const { purpose } = req.params;
  const userId = req.user.id;

  await db.consents.update(
    { user_id: userId, purpose, granted: true },
    { granted: false, withdrawn_at: new Date() }
  );

  // Trigger downstream: detener emails de marketing, deshabilitar tracking
  await eventBus.emit('consent.withdrawn', { userId, purpose });

  res.json({ purpose, granted: false, withdrawn_at: new Date().toISOString() });
});

// Obtener todos los consentimientos del usuario actual
router.get('/consents', async (req, res) => {
  const userId = req.user.id;
  const consents = await db.consents.findActive({ user_id: userId });
  res.json(consents);
});

Auto-eliminación por retención de datos (Python cron)

import logging
from datetime import datetime, timedelta

logger = logging.getLogger(__name__)

RETENTION_POLICIES = {
    'inactive_users': timedelta(days=730),    # 2 años
    'activity_logs': timedelta(days=365),     # 1 año
    'session_data': timedelta(days=30),       # 30 días
    'marketing_data': timedelta(days=540),    # 18 meses
}

def run_retention_cleanup(db):
    """Eliminar o anonimizar datos pasados el período de retención."""
    now = datetime.utcnow()

    for data_type, retention in RETENTION_POLICIES.items():
        cutoff = now - retention
        deleted_count = 0

        if data_type == 'inactive_users':
            # Anonimizar en lugar de eliminar para preservar historial de órdenes
            deleted_count = db.execute("""
                UPDATE users
                SET email = NULL, name = 'Anonymized User',
                    phone = NULL, deleted_at = NOW()
                WHERE last_login < %s AND deleted_at IS NULL
            """, (cutoff,)).rowcount

        elif data_type == 'activity_logs':
            deleted_count = db.execute("""
                DELETE FROM activity_logs
                WHERE created_at < %s
            """, (cutoff,)).rowcount

        elif data_type == 'session_data':
            deleted_count = db.execute("""
                DELETE FROM sessions
                WHERE expires_at < %s
            """, (cutoff,)).rowcount

        logger.info(f'Limpieza de retención: {data_type} - {deleted_count} registros procesados')

    # Loggear entrada de auditoría
    db.execute("""
        INSERT INTO audit_log (action, details, created_at)
        VALUES ('retention_cleanup', %s, NOW())
    """, (f'Completado en {now.isoformat()}',))
const CONSENT_KEY = 'cookie_consent_v1';

function loadConsentBanner() {
  const existing = localStorage.getItem(CONSENT_KEY);
  if (existing) {
    applyConsent(JSON.parse(existing));
    return;
  }

  const banner = document.createElement('div');
  banner.id = 'consent-banner';
  banner.innerHTML = `
    <div class="consent-content">
      <p>Usamos cookies para analytics y marketing. Puedes elegir cuáles habilitar.</p>
      <div class="consent-options">
        <label><input type="checkbox" id="consent-functional" checked disabled> Funcionales (requeridas)</label>
        <label><input type="checkbox" id="consent-analytics"> Analytics</label>
        <label><input type="checkbox" id="consent-marketing"> Marketing</label>
      </div>
      <div class="consent-actions">
        <button id="consent-reject">Rechazar Todas</button>
        <button id="consent-accept">Aceptar Seleccionadas</button>
      </div>
    </div>
  `;
  document.body.appendChild(banner);

  document.getElementById('consent-accept').onclick = () => {
    const consent = {
      functional: true,
      analytics: document.getElementById('consent-analytics').checked,
      marketing: document.getElementById('consent-marketing').checked,
      timestamp: new Date().toISOString(),
    };
    localStorage.setItem(CONSENT_KEY, JSON.stringify(consent));
    applyConsent(consent);
    banner.remove();
  };

  document.getElementById('consent-reject').onclick = () => {
    const consent = {
      functional: true,
      analytics: false,
      marketing: false,
      timestamp: new Date().toISOString(),
    };
    localStorage.setItem(CONSENT_KEY, JSON.stringify(consent));
    applyConsent(consent);
    banner.remove();
  };
}

function applyConsent(consent) {
  if (consent.analytics) {
    loadAnalytics();
  }
  if (consent.marketing) {
    loadMarketingPixels();
  }
}

Mejores Prácticas Adicionales

  1. Implementa un Registro de Procesamiento de Datos (DPR). Rastrea qué datos recolectas, dónde se almacenan, quién accede y la base legal:
CREATE TABLE data_processing_register (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    data_category VARCHAR(100) NOT NULL,    -- 'email', 'location', 'payment'
    purpose TEXT NOT NULL,                   -- 'order fulfillment', 'analytics'
    lawful_basis VARCHAR(50) NOT NULL,       -- 'consent', 'contract', 'legitimate_interest'
    storage_location VARCHAR(200) NOT NULL,  -- 'postgres-eu-west', 's3-eu-central'
    retention_period_days INT NOT NULL,
    third_party_processors TEXT[],           -- ['stripe', 'mailchimp']
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);
  1. Usa tags de clasificación de datos en tu schema. Marca columnas que contienen PII para que los controles de acceso y auditorías puedan apuntarlas:
COMMENT ON COLUMN users.email IS 'PII:GDPR:personal_data';
COMMENT ON COLUMN users.ip_address IS 'PII:GDPR:personal_data:retention_30d';
COMMENT ON COLUMN orders.billing_address IS 'PII:GDPR:personal_data:encrypted';

Errores Comunes Adicionales

  1. Olvidar eliminar datos de los backups. Las solicitudes de olvido requieren remover datos de los backups también. Implementa un proceso para anonimizar datos de usuario en el próximo ciclo de backup, o documenta que los datos de backup expirarán dentro de un período definido:
def schedule_backup_anonymization(user_id):
    """Programar anonimización en el próximo ciclo de backup."""
    # Marcar usuario para anonimización en el próximo backup
    db.pending_anonymization.insert({
        'user_id': user_id,
        'scheduled_for': datetime.utcnow() + timedelta(days=7),
        'status': 'pending',
    })
    # El script de backup revisa esta tabla y anonimiza antes de crear snapshots
  1. No manejar la retirada de consentimiento en tiempo real. Si un usuario retira el consentimiento de marketing, detén los emails inmediatamente. No esperes al próximo batch job:
// Retirada de consentimiento event-driven
eventBus.on('consent.withdrawn', async ({ userId, purpose }) => {
  if (purpose === 'marketing') {
    await emailService.unsubscribe(userId);
    await crmService.removeFromList(userId, 'marketing');
  } else if (purpose === 'analytics') {
    await analyticsService.disableTracking(userId);
  }
});

Preguntas Frecuentes Adicionales

¿Cuál es la diferencia entre data controller y data processor?

Un data controller determina los propósitos y medios del procesamiento de datos personales (ej., tu empresa). Un data processor procesa datos en nombre del controller (ej., AWS, Stripe, Mailchimp). Los controllers son responsables de asegurar que los processors cumplan con GDPR mediante Data Processing Agreements (DPAs).

¿Cuánto tiempo tengo para responder a un DSAR?

GDPR requiere responder dentro de un mes desde la recepción de la solicitud. Esto puede extenderse dos meses para solicitudes complejas, pero debes informar al solicitante dentro del primer mes. Automatiza la exportación de datos para responder más rápido.

¿Qué cuenta como datos personales bajo GDPR?

Cualquier información relacionada con una persona identificada o identificable: nombre, email, dirección IP, identificadores de cookies, datos de ubicación, datos biométricos y datos de comportamiento. Incluso los datos pseudonimizados son datos personales si existe la forma de re-identificar a la persona.