RODO a Twój system - czy dane Twoich klientów są naprawdę bezpieczne

Dostałeś kiedyś od klienta żądanie: "Chcę, żebyście usunęli wszystkie moje dane"? Jeśli Twoja odpowiedź brzmiała "muszę zapytać Tomka, czy to w ogóle możliwe w naszym systemie" - masz problem. I to nie tylko techniczny. Prawny.

RODO (Rozporządzenie o Ochronie Danych Osobowych) obowiązuje od maja 2018 roku. Kary sięgają 4% rocznego obrotu lub 20 milionów euro - zależnie co jest wyższe. Polski UODO nakłada kary regularnie. Ale wiele firm nadal traktuje RODO jako "papierową formalność" - podpisali politykę prywatności i uznali, że sprawa załatwiona.

Sprawa nie jest załatwiona, jeśli Twój system informatyczny nie implementuje RODO na poziomie technicznym. Polityka prywatności bez technicznego zabezpieczenia to jak alarm z wyłączoną syreną.

Jak legacy systemy łamią RODO

Hasła w plain text

Otwórz bazę danych swojego systemu i sprawdź tabelę użytkowników. Jeśli w kolumnie "hasło" widzisz jan_kowalski_2024 zamiast $argon2id$v=19$m=65536,t=3,p=4$... - hasła są przechowywane w czystym tekście.

Co to oznacza:

  • Administrator bazy (Tomek) widzi hasła wszystkich użytkowników
  • Każdy, kto uzyska dostęp do bazy, widzi hasła
  • Jeśli użytkownicy używają tego samego hasła w wielu serwisach (a większość tak robi) - wyciek z Twojej bazy kompromituje ich konta wszędzie

RODO wymaga "wdrożenia odpowiednich środków technicznych" do ochrony danych. Przechowywanie haseł w plain text to absolutne minimum, którego naruszenie jest trudne do obronienia.

Rozwiązanie: Hashowanie haseł algorytmem Argon2 (aktualny standard, zwycięzca Password Hashing Competition). W Elixirze z NIF-em w Rust - hashowanie trwa 200ms, jest odporne na ataki brute-force i rainbow tables.

Brak logów dostępu

Kto wyświetlił dane osobowe klienta Jan Kowalski wczoraj o 15:00? W legacy systemie - nie wiadomo. Nie ma logów. Nie ma audytu. Nie ma śladu.

RODO (art. 30) wymaga rejestru czynności przetwarzania. Jeśli dane wyciekną, musisz wykazać:

  • Kto miał dostęp do danych
  • Kiedy dane były przeglądane
  • Jakie operacje były wykonywane
  • Kto dokonał zmiany lub usunięcia

Bez logów nie wykażesz niczego. A "nie wiemy kto miał dostęp" to najgorsza możliwa odpowiedź przy kontroli UODO.

Rozwiązanie: Audit log w PostgreSQL - każdy dostęp do danych osobowych jest rejestrowany automatycznie:

CREATE TABLE audit_log (
    id BIGSERIAL PRIMARY KEY,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    user_id INTEGER NOT NULL,
    action TEXT NOT NULL,          -- 'view', 'edit', 'delete', 'export'
    table_name TEXT NOT NULL,      -- 'klienci', 'zamowienia'
    record_id INTEGER,
    ip_address INET,
    details JSONB                  -- co dokładnie zmieniono
);

W Phoenix z plugiem - automatyczny zapis bez dodatkowego kodu w każdym kontrolerze. Programista nie musi pamiętać o logowaniu - system robi to za niego.

"Nie da się usunąć" - prawo do zapomnienia

RODO (art. 17) daje osobom prawo żądania usunięcia ich danych. Klient mówi "usuńcie moje dane" i masz na to 30 dni.

W legacy systemie usunięcie klienta oznacza:

  • Powiązane zamówienia tracą referencję → system się sypie
  • Faktury stają się nieczytelne → naruszenie przepisów księgowych
  • Raporty historyczne się rozjeżdżają
  • "Tomek mówi, żeby nie usuwać, bo coś się zepsuje"

Efekt: firma nie może usunąć danych, bo system na to nie pozwala. RODO jest łamane nie ze złej woli, ale z niemożności technicznej.

Rozwiązanie: Pseudonimizacja zamiast usunięcia:

-- Zamiast DELETE - pseudonimizacja danych osobowych
UPDATE klienci SET
    imie = 'USUNIĘTY',
    nazwisko = 'UŻYTKOWNIK',
    email = 'deleted_' || id || '@anonymized.local',
    telefon = NULL,
    adres = NULL,
    gdpr_deleted_at = NOW(),
    gdpr_deleted_by = current_user
WHERE id = 12345;

Zamówienia i faktury zachowują integralność (mają klient_id), ale dane osobowe są usunięte. Raporty historyczne dalej się liczą (kwoty, daty), ale nie zawierają danych osobowych. Pełna zgodność z RODO i przepisami księgowymi jednocześnie.

W Elixirze to osobny moduł z testami, który gwarantuje, że pseudonimizacja jest kompletna - nie pominie kolumny ani tabeli powiązanej.

Brak szyfrowania danych

Baza danych Twojego systemu leży na dysku serwera. Czy dysk jest zaszyfrowany? Czy połączenie z bazą jest szyfrowane? Czy backup jest szyfrowany?

W większości legacy systemów odpowiedź na wszystkie trzy pytania brzmi "nie":

  • Dane at rest - ktoś, kto ukradnie dysk serwera (lub laptop z backupem), ma pełny dostęp do danych klientów
  • Dane in transit - połączenie aplikacji z bazą idzie po sieci nieszyfrowane - każdy w tej samej sieci może podsłuchać
  • Backupy - backup bazy na dysku USB w szufladzie Tomka - niezaszyfrowany, niezabezpieczony

RODO wymaga "pseudonimizacji i szyfrowania danych osobowych" jako środka ochrony (art. 32).

Rozwiązanie w nowoczesnym stacku:

Dane at rest:    PostgreSQL z włączonym szyfrowaniem (TDE)
                 lub szyfrowany filesystem (LUKS/dm-crypt)

Dane in transit: SSL/TLS na połączeniu aplikacja ↔ baza danych
                 (Ecto konfiguracja: ssl: true)

Backupy:         Zaszyfrowane automatycznie (GPG/age)
                 Przechowywane w zaszyfrowanym storage

Wszyscy widzą wszystko

W legacy systemie zwykle jest jeden login i jedno hasło. Albo każdy ma login, ale każdy widzi te same dane. Magazynier widzi marże. Handlowiec widzi dane finansowe. Stażysta widzi bazę klientów z telefonami i adresami.

RODO wymaga zasady minimalizacji dostępu - każdy pracownik powinien widzieć tylko te dane, które są mu niezbędne do pracy.

Rozwiązanie: Row-Level Security w PostgreSQL + role w Phoenix:

-- Handlowiec widzi tylko swoich klientów
CREATE POLICY handlowiec_policy ON klienci
    USING (opiekun_id = current_setting('app.user_id')::int);

-- Magazynier nie widzi danych finansowych
CREATE POLICY magazyn_policy ON zamowienia
    USING (true)  -- widzi zamówienia
    WITH CHECK (true);
-- Ale kolumny marza, cena_zakupu są w osobnej tabeli
-- do której magazynier nie ma dostępu

W Phoenix - system ról z granularnymi uprawnieniami:

# Tylko manager widzi raport marż
def show_margin_report(conn, _params) do
  authorize!(conn, :margin_report, :view)
  # ...
end

Brak zgód i ich historii

Skąd wiesz, że klient wyraził zgodę na przetwarzanie danych? Kiedy ją wyraził? Na co konkretnie? Czy może ją wycofać?

W legacy systemie: "chyba wyraził, bo kupił u nas". To nie jest zgoda w rozumieniu RODO. Zgoda musi być:

  • Dobrowolna - nie może być warunkiem świadczenia usługi
  • Konkretna - na określony cel przetwarzania
  • Świadoma - osoba musi wiedzieć, na co się zgadza
  • Jednoznaczna - aktywne działanie (checkbox, kliknięcie)
  • Wycofywalna - równie łatwa do wycofania jak do udzielenia

Rozwiązanie: Tabela zgód z pełną historią:

CREATE TABLE zgody_rodo (
    id SERIAL PRIMARY KEY,
    klient_id INTEGER NOT NULL REFERENCES klienci(id),
    typ_zgody TEXT NOT NULL,       -- 'marketing_email', 'profilowanie', 'udostepnianie_partnerom'
    udzielona BOOLEAN NOT NULL,
    data_udzielenia TIMESTAMPTZ,
    data_wycofania TIMESTAMPTZ,
    zrodlo TEXT NOT NULL,          -- 'formularz_rejestracji', 'panel_klienta', 'email'
    ip_address INET,
    tresc_zgody TEXT NOT NULL      -- dokładna treść, na którą klient się zgodził
);

Klient w panelu widzi swoje zgody i może je wycofać jednym kliknięciem. System natychmiast przestaje wysyłać materiały marketingowe. Pełna audytowalność: kto, kiedy, na co wyraził zgodę, kiedy ją cofnął.

Koszt niezgodności z RODO

Kary UODO w Polsce

UODO nakłada kary regularnie. Przykłady z ostatnich lat:

  • Morele.net - 2,8 mln PLN za niewystarczające zabezpieczenia techniczne (wyciek danych 2,2 mln klientów)
  • Virgin Mobile - 1,9 mln PLN za brak odpowiednich środków technicznych
  • Fortum Marketing and Sales - 4,9 mln PLN za brak kontroli nad procesorem danych
  • PNP SA - 22 000 PLN za brak współpracy z UODO (nawet mała kara boli małą firmę)

Kary to nie jedyny koszt. Po naruszeniu dochodzi:

  • Koszt powiadomienia poszkodowanych klientów
  • Koszt obsługi prawnej
  • Utrata reputacji (klienci dowiadują się z mediów)
  • Utrata zaufania partnerów biznesowych

Koszt naprawy vs koszt prewencji

Wdrożenie zabezpieczeń RODO w nowym systemie (przy budowie od zera) to 5-10% budżetu projektu. Szyfrowanie, logi, role, pseudonimizacja - to komponenty wbudowane w architekturę.

Dorabianie zabezpieczeń do legacy systemu to 30-50% budżetu nowego projektu - bo trzeba zrozumieć stary system, zidentyfikować wszystkie miejsca z danymi osobowymi, zmodyfikować kod bez łamania istniejących funkcji. Często kosztuje więcej niż migracja do nowego systemu.

Jak nowoczesny system implementuje RODO

Privacy by design (art. 25 RODO)

RODO wymaga, żeby ochrona danych była wbudowana w projekt od dnia zero, nie dorabiana post factum. W Phoenix/Ecto to naturalny sposób budowy:

Konteksty (contexts) - moduł Klienci to jedyny punkt dostępu do danych klientów. Nie da się "przypadkiem" odpytać tabeli klientów z innego modułu, omijając kontrolę dostępu.

Changesets - walidacja danych na wejściu. Email musi być emailem. Telefon musi być telefonem. NIP musi być poprawny. Nieprawidłowe dane nie trafią do bazy.

Testy - test "prawo do zapomnienia" uruchamia się przy każdym buildzie. Jeśli ktoś doda nowe pole z danymi osobowymi i nie uwzględni go w procedurze pseudonimizacji - build się nie uda.

Data retention - automatyczne czyszczenie

RODO mówi: nie przechowuj danych dłużej niż to konieczne. W legacy systemie dane klientów z 2010 roku nadal siedzą w bazie. Nikt ich nie czyści, bo "a nuż się przydadzą".

W nowoczesnym systemie:

# Oban cron - co noc czyść przeterminowane dane
{"0 3 * * *", CzyscPrzeterminowaneDane}

# Worker
def perform(_job) do
  # Usuń logi starsze niż 12 miesięcy
  Audyt.usun_stare_logi(months: 12)

  # Pseudonimizuj klientów nieaktywnych od 3 lat
  Klienci.pseudonimizuj_nieaktywnych(years: 3)

  # Usuń niezakończone rejestracje starsze niż 30 dni
  Rejestracje.usun_niezakonczone(days: 30)

  :ok
end

Automatycznie, co noc, bez udziału człowieka. Dane, które nie są potrzebne, są usuwane zgodnie z polityką retencji.

Raportowanie naruszeń (art. 33)

RODO wymaga zgłoszenia naruszenia do UODO w ciągu 72 godzin. Żeby to zrobić, musisz wiedzieć:

  • Jakie dane wyciekły
  • Ilu osób dotyczą
  • Kiedy doszło do naruszenia
  • Jaki jest potencjalny wpływ

Bez logów i monitoringu - nie wiesz niczego. Z nowoczesnym systemem:

# Wykrycie anomalii - nietypowy dostęp do danych
def detect_anomaly(user_id, action, count) do
  if count > threshold_for(action) do
    Alert.send(:security, """
    Użytkownik #{user_id} wykonał #{count} operacji #{action}
    w ciągu ostatniej godziny. Średnia: #{average_for(action)}.
    """)
  end
end

System sam wykrywa podejrzane zachowania: masowy eksport danych, logowanie z nowego IP, wielokrotne próby dostępu do cudzych danych.

Checklist RODO dla Twojego systemu

Odpowiedz na te pytania:

  • Czy hasła są hashowane (nie plain text)?
  • Czy połączenie z bazą danych jest szyfrowane (SSL)?
  • Czy backup bazy jest szyfrowany?
  • Czy każdy dostęp do danych osobowych jest logowany?
  • Czy możesz usunąć/pseudonimizować dane klienta na żądanie?
  • Czy masz rejestr zgód RODO z historią?
  • Czy pracownicy mają dostęp tylko do danych niezbędnych im do pracy?
  • Czy masz procedurę zgłaszania naruszeń w 72 godziny?
  • Czy dane są automatycznie czyszczone po okresie retencji?
  • Czy system loguje, kto zmienił jakie dane i kiedy?

Jeśli odpowiedziałeś "nie" na więcej niż 3 pytania, Twój system ma luki w zgodności z RODO. Każde "nie" to potencjalna podstawa do kary od UODO.

Chcesz zrobić audyt RODO swojego systemu? Porozmawiajmy - sprawdzimy luki i zaproponujemy plan naprawy, zanim UODO to zrobi za nas.