MyÚčto.cz Manuál
Stáhnout PDF Příručky Zpět na hlavní stránku

101. Bezpečnost (MFA, passkeys, zámek session, IP allowlist, role, activity log)

Návod pro uživatele a správce: jak si zabezpečit přihlášení (TOTP, přístupové klíče, záložní kódy), jak správce vynutí silné ověření, omezí přístup podle IP adres, nastaví role a zkontroluje activity log. Na konci jsou podrobná pravidla a provozní postupy.

Bezpečnost MyÚčto stojí na několika navazujících vrstvách:

  1. Autentizace - heslo (bcrypt + pepper) nebo volitelně passkey bez hesla, brute-force ochrana a CAPTCHA
  2. Silné MFA - passkey nebo TOTP
  3. Síťová izolace - IP allowlist (volitelný, doporučeno v produkci)
  4. Autorizace - databázové role s oprávněními neviditelné / čtení / zápis
  5. Audit - activity log všech mutací
  6. Zámek session - serverové uzamčení PWA po nečinnosti

101.1 Kdy to potřebujete

Kapitolu otevřete, když:

KdyCo udělatKde
při prvním přihlášeníZapnout TOTP nebo registrovat přístupový klíčProfil → 2FA / TOTP, Profil → Přístupové klíče
hned po zapnutí druhého faktoruVygenerovat a uložit záložní kódyProfil → Přístupové klíče, Záložní kódy
při nasazeníVynutit MFA, nastavit IP allowlist a trusted_proxiescfg.php, viz § 101.6
při novém uživateliPřidělit mu roliSystém → Role a oprávnění, Systém → Uživatelé
jednou měsíčněProjít activity logSystém → Log
před předáním dat vývojářiVyrobit anonymizovanou kopiiviz § 101.11.10

101.2 Než začnete

101.3 Krok za krokem: zapnutí TOTP

  1. Otevřete Profil → 2FA / TOTP.
  2. Zadejte aktuální heslo a klikněte na Nastavit TOTP. Máte-li už přístupový klíč, klikněte místo toho na Ověřit přístupovým klíčem a nastavit TOTP.
  3. Aplikace ukáže QR kód a textový klíč. V autentikátoru zvolte přidat účet a naskenujte QR kód.
  4. Zadejte aktuální šestimístný kód z autentikátoru a klikněte na Aktivovat TOTP.

Jak poznáte, že je hotovo: Záložka 2FA / TOTP ukazuje TOTP je aktivní. a při dalším přihlášení aplikace požádá o kód z autentikátoru.

Tip

Hned potom si vygenerujte záložní kódy (§ 101.5). Při ztrátě autentikátoru jinak zbývá jen zásah správce na serveru.

101.4 Krok za krokem: registrace přístupového klíče

  1. Otevřete Profil → Přístupové klíče.
  2. Do pole Název přístupového klíče napište název (například název telefonu) a klikněte na Přidat přístupový klíč.
  3. Potvrďte registraci systémovým dialogem zařízení (otisk, obličej, PIN nebo bezpečnostní klíč). U prvního klíče zadejte aktuální heslo, nebo TOTP kód.
  4. Doporučujeme zaregistrovat druhý klíč, nebo mít vedle klíče aktivní TOTP.

Jak poznáte, že je hotovo: Klíč je v seznamu s datem Vytvořeno a po použití s datem Naposledy použito. Klíč lze Přejmenovat nebo Odebrat.

101.5 Krok za krokem: záložní kódy

  1. Otevřete Profil → Přístupové klíče a v části Záložní kódy klikněte na Vygenerovat záložní kódy. Potvrďte ověření přístupovým klíčem nebo kódem z autentikátoru.
  2. Kódy si hned uložte (Stáhnout jako soubor, Kopírovat, nebo tisk) mimo počítač, ze kterého se přihlašujete. Zobrazí se jen jednou.
  3. Klikněte na Mám je uložené.

Jak poznáte, že je hotovo: Sekce ukazuje Zbývá 10 z 10 kódů. Při ztrátě klíče i autentikátoru zadáte kód na přihlašovací stránce místo druhého faktoru (odkaz „Nemám klíč ani autentikátor“). Každý kód funguje právě jednou.

Upozornění

Nová sada okamžitě zruší tu předchozí. Kód neumožní vytvořit API token ani vydat další sadu kódů; nejdřív je potřeba obnovit skutečný faktor.

101.6 Krok za krokem: vynucení silného MFA (správce)

  1. V cfg.php (nebo cfg.local.php) nastavte:
   'auth' => [
       'require_mfa' => true,
       'allowed_mfa_methods' => ['passkey', 'totp'],
   ],

V Dockeru a PaaS stejné nastavíte proměnnými MYINVOICE_AUTH_REQUIRE_MFA=true a MYINVOICE_AUTH_MFA_METHODS=passkey,totp.

  1. Uživatelé bez silného faktoru se při dalším přihlášení dostanou na stránku pro nastavení MFA a bez ní nepokračují.
  2. Chcete-li povolit přihlášení jen přístupovým klíčem, nastavte auth.passwordless_login.enabled (viz § 101.11.2.3).

Jak poznáte, že je hotovo: Nový uživatel po přihlášení heslem vidí /setup-mfa a do aplikace se dostane až po registraci klíče nebo zapnutí TOTP.

101.7 Krok za krokem: omezení přístupu IP allowlistem (správce)

  1. Do cfg.php zapište povolené IP adresy a rozsahy (příklad je v § 101.11.4). Vždy ponechte svou IP, VPN a záložní hotspot.
  2. Běží-li aplikace za reverse proxy, uveďte proxy do trusted_proxies (§ 101.11.4.1) a zajistěte, aby edge proxy přepisovala X-Forwarded-For (§ 101.11.4.2).
  3. Ověřte z internetu: curl -H 'X-Forwarded-For: 1.2.3.4' https://vas-server/api/health. Pak v Systém → Log musí být vaše skutečná IP, ne 1.2.3.4.

Jak poznáte, že je hotovo: Cizí IP dostane 403 a v logu jsou skutečné IP klientů.

Upozornění

IP allowlist je záměrně jen v cfg.php, ne v UI. Při omylu byste se zablokovali a nešel by sundat přes UI.

101.8 Krok za krokem: přidělení role uživateli (superadmin)

  1. Otevřete Systém → Role a oprávnění. Novou roli založíte tlačítkem Nová role, existující upravíte (Upravit) nebo zkopírujete (Duplikovat).
  2. Zadejte Název role, Typ role a u každého modulu úroveň Neviditelné, Pouze čtení nebo Zápis. Klikněte na Uložit.
  3. Otevřete Systém → Uživatelé a uživateli přiřaďte roli a firmy.

Jak poznáte, že je hotovo: Uživatel po přepnutí na firmu vidí v menu jen moduly, na které má právo. Role se u firmy nesčítají; chybějící oprávnění znamená zákaz.

101.9 Krok za krokem: kontrola activity logu

  1. Otevřete Systém → Log.
  2. Filtrujte podle akce a entity. U záznamu uvidíte čas, uživatele, akci, entitu, IP adresu a payload.
  3. Hledejte neúspěšná přihlášení (auth.login_failed) a neočekávané úpravy.

Jak poznáte, že je hotovo: Podezřelé záznamy jste vysvětlili nebo eskalovali. Doporučujeme kontrolu aspoň jednou měsíčně.

101.10 Když něco nejde

Co vidítePročCo udělat
Nemáte autentikátor ani klíčZařízení je ztracenéZadejte záložní kód (§ 101.5); jinak správce spustí php api/bin/reset-mfa.php <email> (§ 101.11.2.4)
Odebrat poslední silný faktor nejdeJe vynucené MFANejdřív zaregistrujte druhý klíč nebo zapněte TOTP
Přístupové klíče nejsou dostupnéapp.url není HTTPS DNS adresa, nebo je v konfiguraci vypnutá metoda passkeyOpravte app.url (§ 101.11.2.1)
Přihlášení hlásí 503 passkeys_unavailableWebAuthn je nedostupný a účet nemá jiný druhý faktorOpravte app.url, případně reset-mfa.php
TOTP kód nejde použít podruhéKód platí jen jednouPočkejte na nový kód v autentikátoru
Lockout po opakovaných chybáchBrute-force ochrana (§ 101.11.3)Počkejte, až lockout vyprší
Zamčená aplikace po nečinnostiZámek sessionKlikněte na Odemknout přístupovým klíčem, nebo na Odhlásit a přihlaste se znovu
Uživatelé bez TOTP se nepřihlásí (e-mailové OTP)Nechodí e-mailyOpravte SMTP, nebo vypněte email_otp.enabled
V logu je pořád IP proxyChybí trusted_proxiesViz § 101.7
Chyba 403 csrf_failed / origin_mismatchChybí Origin nebo CSRF tokenObnovte stránku a přihlaste se znovu (§ 101.11.6)

101.11 Podrobnosti a pravidla

101.11.1 Hesla

VrstvaDetail
Algoritmusbcrypt cost 12
PepperSůl z cfg.php → app.pepper (32B base64), neukládá se v DB
Min. délka12 znaků
Max. délkaBez limitu - passphrase je doporučená (20+ znaků)
Kontrola sílyIndikátor v UI (slabé / střední / silné)
Reset heslaOdkaz na 1 hodinu, e-mailem
Tip

Passphrase je bezpečnější než krátké složité heslo. „korelace medvědí dýně přístav 2026" má 49 znaků a je odolnější vůči brute-force než „Hu1@n!22".

101.11.2 Vícefaktorové ověření

MyÚčto podporuje dva silné faktory:

E-mailové OTP je kompatibilní druhý krok pro účet bez silného faktoru, ale nesplňuje povinnou silnou MFA politiku. Důvěryhodné zařízení se týká pouze e-mailového OTP.

101.11.2.1 Passkeys

Passkey zaregistrujete v Profil → Přístupové klíče. Každý klíč má vlastní název, datum vytvoření a posledního použití. Lze jej přejmenovat nebo odvolat. Aplikace podporuje více klíčů; doporučené jsou dvě passkeys nebo jedna passkey spolu s TOTP.

Passkey se používá:

Systémový dialog může podle zařízení použít otisk, obličej, PIN, gesto, heslo zařízení nebo externí bezpečnostní klíč. MyÚčto konkrétní metodu nezjišťuje, biometrická data neopouštějí zařízení a server ukládá pouze veřejný klíč. Poskytovatel platformy nebo password manager může passkey end-to-end šifrovaně synchronizovat mezi zařízeními.

Passkeys vyžadují stabilní veřejnou URL. V produkci musí app.url obsahovat přesný HTTPS origin, například https://faktury.example.cz. Klíč je svázaný s hostname; po změně domény jej na nové doméně nelze použít. Pro lokální vývoj je podporované http://localhost, nikoli běžný HTTP přístup přes LAN IP.

Provozní diagnostika canonical app.url

app.url je současně canonical origin pro běžné routování, odkazy a WebAuthn. Pro pravidelný monitoring vždy volejte health přes přesný origin z app.url, ne přes náhodnou IP nebo alternativní Host hlavičku. Například pro app.url = https://faktury.example.cz:

curl --fail --silent --show-error https://faktury.example.cz/api/v1/health

HTTP 200 pouze potvrzuje, že endpoint odpověděl. Monitoring má v JSON zvlášť kontrolovat db, podle nasazení redis a configuration.app_url. Poslední objekt je veřejný a neobsahuje nastavenou URL ani hostname, userinfo, heslo, cestu, query nebo fragment:

state / reason_codeRoutování a náprava
missing / app_url_missingKlíč chybí, je přesně prázdný nebo obsahuje jen whitespace. Chybějící a přesně prázdná hodnota zachovává legacy fallback na validní request hostname; whitespace tento fallback nemá. Po setupu nastavte explicitní HTTP(S) origin.
invalid / app_url_invalid_originNeprázdná hodnota není samostatný HTTP(S) origin. Pokud z ní legacy resolver ještě získá platný hostname, uzná nejvýše request s přesně stejným hostname, nikdy libovolný host; nejde však o podporovaný canonical origin a musí se opravit. Odstraňte userinfo, cestu, query či fragment nebo opravte schéma, hostname a port.
routing_only / app_url_webauthn_incompatibleBěžné rozhraní funguje, včetně záměrného HTTP nebo LAN-IP nasazení, ale passkeys nejsou dostupné. Pro WebAuthn použijte HTTPS DNS hostname.
hostname_conflict / app_url_hostname_conflictHostname z app.url je současně uložený jako vlastní doména firmy. Běžné cesty aplikace jsou fail-closed; přesný read-only health zůstane dostupný. Obnovte původní canonical adresu, vlastní doménu deaktivujte a smažte, nebo nastavte jiný canonical hostname.
webauthn_ready / app_url_validOrigin vyhovuje routování i WebAuthn. Vedle HTTPS DNS originu je povolená jediná HTTP výjimka: http://localhost.

Při whitespace-only nebo jiné neprázdné hodnotě nepoužitelné pro routování propustí tenant host gate přes jiný hostname jen přesné GET a HEAD /api/v1/health (interně /api/health). POST, jiný endpoint, přihlášení ani ostatní aplikační cesty výjimku nedostanou. Toto je pouze recovery cesta; po opravě se health znovu monitoruje přes nakonfigurovaný canonical hostname. Pokud cizí hostname odmítne už reverse proxy, spusťte recovery dotaz ze serveru nebo kontejneru přes hostname, který proxy přijímá. Health neobchází zapnutý IP allowlist. Během nedokončeného first-run setupu používejte GET; setup allowlist metodu HEAD nepovoluje.

Stejná přesná health výjimka platí při kolizi canonical hostname s uloženou vlastní doménou. Na rozdíl od syntakticky neplatného app.url ji volejte přes hostname z app.url; všechny ostatní cesty na něm zůstanou odmítnuté.

First-run setup doplní z otevřeného originu chybějící, prázdnou či whitespace-only hodnotu a známé distribuční placeholdery. Jinou explicitně neprázdnou neplatnou hodnotu nepřepisuje: preflight ji označí jako chybu, aby ji správce opravil v cfg.php, cfg.local.php nebo přes MYINVOICE_APP_URL vědomě.

Runtime zapíše pro stavy s routing_compatible: false serverový warning configuration.app_url_unusable. Kontext obsahuje jen stabilní state a reason_code; původní ani odvozená hodnota konfigurace se neloguje. Umístění logu určuje logging.path. Podrobný recovery postup je v § 999.5.1.8.

Vlastní domény klientských portálů se nestávají dalším WebAuthn RP ID. Browser se z nich přesměruje na přesný canonical origin z app.url, kde proběhne passwordless passkey, passkey jako druhý faktor nebo TOTP. Aplikace potom vydá jednorázový kód platný 60 sekund, svázaný s PKCE verifierem, uživatelem, firmou a přesným cílovým hostnamem. Kód lze spotřebovat jen jednou a skutečný session token se v URL nikdy neobjeví. Na cílové doméně vznikne samostatná host-only session; správa passkeys a ostatní interní obrazovky zůstávají na canonical originu. Přímé WebAuthn operace na vlastní doméně server odmítne, včetně správy klíčů a options/verify pro odemčení session. Zamykací obrazovka místo nich zahájí nové ověření na canonical originu a po jednorázovém PKCE návratu vytvoří pro vlastní doménu novou host-only session.

Přidání a odvolání passkey vyžaduje nové ověření passkey nebo TOTP. U účtu bez dosavadního silného faktoru první registrace vyžádá aktuální heslo. Při povinném MFA nelze odvolat poslední povolený silný faktor.

Pokud správce přechází z TOTP na passkeys a vyřadí TOTP ze seznamu povolených metod, uživatel smí existujícím TOTP potvrdit pouze registraci své první passkey. Přechod je dostupný jen tehdy, když jsou passkeys povolené a účet ještě nemá žádnou aktivní passkey. Stejné omezení platí pro registraci heslem u účtu bez dosavadního silného faktoru. Server pod databázovým zámkem znovu ověří, že jde skutečně o první klíč, takže nelze předem otevřít více registrací a dokončit je až po přidání prvního klíče. Další klíče už vyžadují aktuálně povolený faktor.

TOTP = time-based one-time password (RFC 6238).

101.11.2.2 Aktivace TOTP

Profil → 2FA / TOTP, tlačítko Nastavit TOTP.

Aktivace 2FA
Aktivace 2FA
  1. Nejdřív znovu prokážete, že jste to vy: bez passkey aktuálním heslem, s aktivní passkey ověřením passkey (stejně jako při registraci dalšího klíče). Samotná přihlášená session na přidání druhého faktoru nestačí - jinak by si ho na unesené session mohl založit útočník. Špatně zadaná hesla se tu počítají do stejné ochrany proti hádání jako při přihlášení, takže po opakovaných chybách se účet na 15 minut zamkne i pro login.
  2. Aplikace ukáže QR kód + textový secret key.
  3. V mobilu otevřete autentikátor (Proton Authenticator, Ente Auth, Google Authenticator, Microsoft Authenticator, 1Password, Bitwarden) → Přidat účet → Sken QR kódu.
  4. Aplikace začne generovat 6-cifrené kódy každých 30 sekund.
  5. Zadejte aktuální kód do MyÚčta a klikněte na Aktivovat TOTP.

Na vlastní doméně klientského portálu se nastavení TOTP otevře na hlavní adrese aplikace, kde lze bezpečně ověřit také existující passkey.

Pokud průvodce nabídne nebo vyžádá první TOTP bezprostředně po přihlášení heslem nebo po prvním nastavení hesla z uvítacího odkazu, heslo znovu neopisujete. Totéž platí při prvotním nastavení vlastní instalace. Pokračování je jednorázové a platí nejvýše pět minut. Po obnovení stránky, opuštění průvodce nebo změně hesla je potřeba nové ověření. Existující passkey tato možnost nenahrazuje.

Při přechodu politiky z passkeys na TOTP lze existující passkey použít výhradně k potvrzení zřízení TOTP. Nepovoluje to přihlášení ani jiné citlivé operace metodou, kterou správce zakázal.

Změna nebo reset hesla, včetně změny správcem, ruší rozpracovanou aktivaci TOTP. Začněte znovu a načtěte nový QR kód. Již aktivní TOTP se změnou hesla neruší.

Tip

Při ztrátě autentikátoru použijte jinou passkey nebo záložní kód (viz § 101.11.2.4). Až když nemáte nic z toho, zbývá CLI rescue php api/bin/reset-mfa.php <email>.

101.11.2.3 Přihlášení s passkey a MFA

Po zadání e-mailu a hesla nabídne aplikace passkey, pokud ji účet má. Je-li aktivní také TOTP, lze explicitně přepnout na šestimístný kód z autentikátoru.

TOTP kód lze použít jen jednou pro přihlášení nebo potvrzení citlivé operace. Pokud hned po přihlášení vytváříte API token nebo přidáváte další passkey, počkejte na nový kód v autentikátoru.

2FA výzva
2FA výzva

Správce může navíc explicitně povolit přihlášení pouze pomocí passkey:

'auth' => [
    'passwordless_login' => [
        'enabled' => true,
    ],
],

Totéž lze nastavit přes ENV:

MYINVOICE_AUTH_PASSWORDLESS_LOGIN=true

Výchozí hodnota je false, takže aktualizace nezmění dosavadní přihlašování. Funkce je dostupná jen tehdy, když auth.allowed_mfa_methods obsahuje passkey a WebAuthn konfigurace je platná. Přihlašovací stránka potom nabídne Přihlásit přístupovým klíčem. Browser zobrazí passkeys pro aktuální doménu a vybraný klíč bezpečně předá identitu účtu; e-mail ani heslo se neposílají. Ověření uživatele na zařízení je povinné a úspěšná passkey rovnou vytvoří silně ověřenou session, bez dalšího TOTP.

Passwordless režim neodstraňuje heslo ani standardní formulář. Ten zůstává fallbackem pro jiné zařízení a cestou k TOTP. Pokud passkey není dostupná, zrušte systémový dialog a přihlaste se e-mailem a heslem.

Účet s passkey nedostane automatický fallback na e-mailový kód. Pokud passkey na aktuálním zařízení není dostupná, použijte jinou passkey, TOTP nebo rescue.

101.11.2.4 Obnova přístupu

Kde passkey fyzicky leží, rozhoduje o tom, co se stane při ztrátě zařízení:

Kam se klíč uloží, vybírá prohlížeč při registraci; aplikace to neřídí a ani to nezjistí zpětně. Máte-li jediný klíč a ten je vázaný na zařízení, mějte jako zálohu buď druhý klíč, nebo aktivní TOTP.

Hned po registraci aplikace požádá o jedno zkušební ověření novým klíčem. Některé správce klíčů registraci projdou, ale ověření pak vracejí neplatné; takový klíč aplikace rovnou odebere a vyzve vás, abyste ho uložili jinam nebo si zapnuli TOTP. Platí to i při prvním nastavení účtu z uvítacího e-mailu: když klíč neprojde, zůstanete na obrazovce nastavení ověření a vyberete jiný klíč nebo TOTP. Zavřete-li systémový dialog, klíč zůstane uložený jako neověřený a do 15 minut od registrace ho můžete vyzkoušet tlačítkem Vyzkoušet v seznamu klíčů.

Záložní jednorázové kódy

Profil → Přístupové klíče → Záložní kódy. Sada deseti kódů ve tvaru ABCDE-FGHJK; každý funguje právě jednou. Zadávají se na přihlašovací stránce místo passkey i TOTP (odkaz „Nemám klíč ani autentikátor“). Po přihlášení jimi potvrdíte i obnovu faktoru: na stránce Přístupové klíče odkaz „Klíč nefunguje? Použít záložní kód“ zpřístupní odebrání klíče i přidání nového a na stránce 2FA / TOTP stejný odkaz zapnutí TOTP. To se hodí, když správce klíčů vrací neplatné ověření a nový klíč by skončil v témže správci. Kód se spotřebuje, i když operaci nedokončíte.

Odebrat poslední silný faktor nejde ani záložním kódem, pokud je MFA povinné. Nejdřív zapněte TOTP nebo přidejte nový klíč, teprve potom starý klíč odeberte.

Server ukládá jen SHA-256 kódu, takže sadu jde zobrazit jedinkrát - při vygenerování. Uložte ji mimo počítač, ze kterého se přihlašujete: tisk, trezor, správce hesel. Vygenerování nové sady okamžitě ruší tu předchozí.

Co kód schválně neumí, aby zůstal záchranou a nestal se trvalým faktorem:

Použití kódu se zapisuje do activity logu (auth.recovery_code_login, auth.recovery_code_used) i s IP a počtem zbývajících kódů. Neúspěšné ověření přístupovým klíčem zapisuje auth.passkey_step_up_failed (při přihlášení auth.login_failed) s kategorií důvodu, např. assertion_invalid s hláškou validátoru nebo unknown_credential, podle které se dá dohledat, proč klíč neprošel.

Rescue na serveru

Nejprve použijte jinou zaregistrovanou passkey, TOTP nebo záložní kód. Pokud není dostupné nic z toho, správce může na serveru spustit:

php api/bin/reset-mfa.php vas@email.cz

Skript vypne TOTP, odvolá všechny passkeys, zruší důvěryhodná zařízení, čekající OTP, WebAuthn flow, step-up proofy i záložní kódy a invaliduje všechny session uživatele. Zruší také všechny API tokeny a MCP připojení uživatele a vypíše jejich počet; integrace je po obnově účtu potřeba napojit novým tokenem. Stejný skript lze spustit také přes alias reset-2fa.php.

Běžná změna hesla ani obnova zapomenutého hesla API tokeny neruší: tokeny slouží integracím, které na hesle nezávisí. Spravují a ruší se samostatně v aplikaci.

Docker

V kontejneru je aplikace v /var/www/html a běží pod www-data. Spouštějte skript pod tímto uživatelem - jako root sice projde taky, ale případné soubory, které by po sobě zanechal, by pak měly špatného vlastníka:

# docker compose (název služby `app` dle docker-compose.yml)
docker compose exec -u www-data app php api/bin/reset-mfa.php vas@email.cz

# samostatný kontejner
docker exec -u www-data -w /var/www/html <container> php api/bin/reset-mfa.php vas@email.cz

Ověření, že reset opravdu proběhl (řádek auth.mfa_reset nese i jméno účtu, pod kterým se skript spustil):

docker compose exec -u www-data app \
  php -r 'require "api/vendor/autoload.php";
    $c = MyInvoice\Bootstrap::buildApp()->getContainer();
    $pdo = $c->get(MyInvoice\Infrastructure\Database\Connection::class)->pdo();
    foreach ($pdo->query("SELECT created_at, payload FROM activity_log
                           WHERE action = \"auth.mfa_reset\"
                           ORDER BY id DESC LIMIT 5") as $r) {
        echo $r["created_at"], "  ", $r["payload"], PHP_EOL;
    }'
Upozornění

Rescue používejte jen z důvěryhodného shellu serveru. Přímý SQL zásah není ekvivalentní: snadno ponechá aktivní session nebo rozpracované ověřovací flow. Reset je zapsaný do auditní stopy a zapečetěný v hash-chainu (§ 33a) - kdo ho spustil a odkud, tedy zpětně dohledáte.

101.11.2.5 Vynucení silného MFA

Pokud chcete, aby každý uživatel měl passkey nebo TOTP, nastavte v cfg.php (nebo cfg.local.php):

'auth' => [
    'require_mfa' => true,
    'allowed_mfa_methods' => ['passkey', 'totp'],
],

Stejné lze přepnout přes ENV (Docker / PaaS):

MYINVOICE_AUTH_REQUIRE_MFA=true
MYINVOICE_AUTH_MFA_METHODS=passkey,totp

Úvodní wizard nabízí jen přepínač „vyžadovat silné MFA"; seznam metod nechává na konfiguraci, takže po instalaci jsou povolené obě. Jeho zúžení je vědomý zásah do cfg.php / ENV.

Chování:

Starší auth.require_totp = true a MYINVOICE_AUTH_REQUIRE_TOTP=true zůstávají podporované jako TOTP-only politika. Pro nové instalace používejte obecné MFA nastavení.

allowed_mfa_methods rozhoduje co povinné MFA splní, ne na co se přihlášení zeptá. Zúžení seznamu (typicky na ['passkey'] při přechodu na passkey-only) proto nikdy nezruší faktor, který uživatel reálně má:

Neznámá hodnota v seznamu (například email_otp, které sem nepatří) start aplikace neshodí: použije se výchozí ['passkey', 'totp'] a přihlášený správce uvidí na health endpointu warning mfa_methods_configuration.

Upozornění

Povolení TOTP vyžaduje validní app.secret_encryption_key (32B base64). Health endpoint na chybnou konfiguraci upozorní; viz § 999 Řešení problémů.

101.11.2.6 E-mailové ověření pro účet bez silného faktoru

Pro uživatele, kteří nechtějí (nebo neumí) authenticator aplikaci - typicky externí účetní - lze zapnout e-mailové OTP jako druhý faktor. Kdo nemá aktivní passkey ani TOTP, dostane po zadání hesla 6místný kód na e-mail a musí ho opsat.

Zapnutí v cfg.php (výchozí stav je vypnuto):

'auth' => [
    'email_otp' => [
        'enabled'                 => true,  // kód jen pro účet bez passkey i TOTP
        'code_ttl_minutes'        => 10,    // platnost kódu
        'max_attempts'            => 5,     // pokusů na jeden kód, pak je nutný nový
        'resend_cooldown_seconds' => 60,    // min. prodleva mezi odesláním nového kódu
        'trusted_device_days'     => 30,    // „zapamatovat toto zařízení" na kolik dní
        'trusted_cookie_name'     => '__Host-myinvoice_td',
    ],
],

Chování:

Upozornění

Vyžaduje funkční SMTP. Když e-maily nechodí, uživatelé bez TOTP se nepřihlásí - buď opravte SMTP, nebo nastavte enabled => false. Nouzově lze uživateli zrušit i důvěryhodná zařízení a čekající kódy: php api/bin/reset-mfa.php <email>.

101.11.2.7 Serverový zámek session

Automatický zámek browserové a PWA session je ve výchozím stavu vypnutý, aby se po aktualizaci nezměnilo chování existujících instalací. Správce nastavuje výchozí timeout pomocí session.lock_after_minutes nebo MYINVOICE_SESSION_LOCK_AFTER_MINUTES. Hodnota 0 znamená, že správce zámek nevynucuje. Uživatel jej přesto může dobrovolně zapnout v profilu na záložce Zámek aplikace.

Hodnota musí být celé číslo od 0 do 1440; podporovaný je i kanonický numerický řetězec, například "15". Neplatná hodnota nesmí zablokovat start aplikace: výchozí automatický zámek se bezpečně vypne a přihlášený uživatel uvidí upozornění session_lock_configuration na health endpointu. Osobní explicitně nastavené intervaly zůstávají účinné.

Osobní nastavení má tyto hranice:

Ruční Zamknout v uživatelském menu je dostupné bez ohledu na timeout, ale jen pokud má účet alespoň jednu aktivní passkey a instalace ji umí použít. Bez dostupné passkey se tlačítko nezobrazuje a server přímý požadavek odmítne, aby nevznikla session, kterou lze ukončit pouze úplným odhlášením.

Stejnou podmínku má i osobní interval: kladnou hodnotu server uloží jen účtu s použitelnou passkey, jinak vrátí 400 validation_failed. Volba *Použít nastavení správce* zůstává dostupná vždy.

Upozornění

Správcovská hodnota session.lock_after_minutes > 0 platí pro všechny účty, i pro ty bez passkey - a ty pak zamčenou session jen odhlásí (rozepsaný formulář se ztratí). Typicky se to týká instalací, kde uživatelé jedou na e-mailovém OTP. Aplikace na to upozorní health warningem session_lock_without_unlock_method; buď uživatelům zaregistrujte passkey, nebo nechte session.lock_after_minutes = 0 a osobní volbu na nich.

Aktivitu posouvají pouze skutečné vstupy do viditelné soukromé stránky, například kliknutí, dotyk nebo klávesa. Polling, běžné API requesty, focus okna ani service worker timeout neposouvají. Po dosažení limitu backend označí session jako zamčenou a odmítne business API i v případě, že někdo odstraní frontendový overlay.

Odemčení vyžaduje passkey a rotuje session ID i CSRF token, přičemž zachová původní absolutní expiraci. TOTP existující zamčenou session přímo neodemkne; tlačítko Odhlásit na zamykací obrazovce provede bezpečný logout a potom stačí celé přihlášení.

Zámek omezuje náhodný přístup k odloženému odemčenému zařízení. Nechrání data, která už přečetl malware nebo XSS během aktivní session. Webová PWA negarantuje zákaz screenshotu ani skrytí Android Recents. Rozpracovaný formulář zůstane zachovaný jen dokud prohlížeč stránku drží v paměti; po ukončení stránky Androidem se neuložená data ztratí. Offline odemčení není možné, protože server musí vydat a ověřit jednorázovou challenge.

101.11.2.8 Nasazení změny autentizačního modelu

Aktivní session vytvořené před doplněním autentizačního kontextu se po migraci označí jako legacy; migrace z pouhé existence TOTP neodvozuje, že konkrétní session druhý faktor skutečně ověřila. Pokud instalace vyžaduje MFA, uživatelé s takovou session se proto musí jednou znovu přihlásit. Jde o záměrné fail-closed chování, které brání povýšení staré session bez důkazu o MFA. Přihlašovací endpointy přítomnou starou cookie ignorují, takže stačí dokončit standardní login; cookie není nutné ručně mazat v nastavení prohlížeče.

Browser session a její stav zámku jsou autoritativně uložené v MariaDB. Redis slouží pro rate limiting, brute-force ochranu a best-effort cache; jeho výpadek nesmí obnovit odvolanou, nahrazenou nebo zamčenou session.

Z toho plyne jedna změna configu: session.driver už se nepoužívá. Starší cfg.php ho může dál obsahovat ('auto' / 'redis' / 'db'), hodnota se ale ignoruje - session vždy čte a zapisuje MariaDB. Klíč lze bez náhrady smazat.

Migrace 0145 přestavuje tabulku sessions (dvanáct nových sloupců, backfill a tři indexy), takže po dobu jejího běhu je tabulka zamčená a přihlašování nefunguje. Naměřeno na MariaDB 11.8: ~16 s na 300 000 session, u běžných instalací s jednotkami až stovkami řádků je to pod sekundu. Před upgradem se vyplatí spustit php api/bin/cron-cleanup.php, ať se nepřestavují dávno expirované řádky.

101.11.3 Brute-force ochrana

Pokusy běhemAkce
5 selhání / 5 minutCAPTCHA (Cloudflare Turnstile)
10 selhání / 15 minutLockout 15 minut (per IP)
30 selhání / 1 hodinuLockout 24 hodin + e-mail uživateli o pokusech

Implementace: Redis pokud běží, jinak MariaDB MEMORY engine fallback.

101.11.4 IP allowlist (volitelné)

V cfg.php → ip_allowlist.allow můžete omezit přístup jen na vybrané IP / CIDR rozsahy.

'ip_allowlist' => [
    'enabled' => true,
    'mode' => 'block',           // 'block' = ne-allowlisted IP dostane 403
    'allow' => [
        '127.0.0.1',
        '203.0.113.42',          // vaše kancelářská WAN (IPv4)
        '2001:db8:1234::/48',    // IPv6 prefix
    ],
],

Doporučení v produkci:

Tip

IP allowlist je v cfg.php (file-based config) → změna vyžaduje SSH / deploy. Není v UI schválně - v případě omylu byste se zablokovali a nemohli ho přes UI sundat.

101.11.4.1 Za reverse proxy: trusted_proxies (důležité)

Pokud aplikace běží za reverse proxy (doporučené produkční nasazení - viz Instalace Docker), vidí všechny požadavky přicházet z IP proxy (např. brána Dockeru 172.x.0.1), ne od reálného klienta. Bez konfigurace pak:

Proto za reverse proxy uveďte proxy do trusted_proxies - aplikace pak vezme skutečnou klientskou IP z hlavičky X-Forwarded-For:

'ip_allowlist' => [
    'trusted_proxies' => [
        '172.16.0.0/12',         // Docker bridge sítě
        // '10.0.0.0/8',         // nebo konkrétní IP/rozsah vaší proxy
    ],
    'header' => 'X-Forwarded-For', // výchozí; odkud číst reálnou IP (jen za trusted proxy)
],
Upozornění

Do trusted_proxies patří jen IP/rozsahy proxy, kterým věříte - klient za nedůvěryhodnou proxy by jinak mohl X-Forwarded-For podvrhnout. Aplikace hlavičku respektuje pouze tehdy, když REMOTE_ADDR odpovídá trusted_proxies.

101.11.4.2 Edge proxy MUSÍ X-Forwarded-For přepisovat, ne appendovat

Tohle je nejčastější a nejzávažnější chyba v nasazení za proxy. X-Forwarded-For je obyčejná klientská hlavička - kdokoli ji může poslat s libovolným obsahem:

curl -H 'X-Forwarded-For: 203.0.113.42' https://vas-server/api/...

Aplikace chain prochází zprava a odloupává známé trusted hopy, takže podvržené položky *nalevo* jsou neškodné. Ale to je bezpečné jen tehdy, když edge proxy klientskou hodnotu zahodí. Když ji jen appenduje (nebo ji nesahá vůbec), zůstane v chainu obsah od útočníka a ten si může zvolit, jakou IP aplikace uvidí → obejití IP allowlistu, obejití brute-force lockoutu a podvržené auditní logy.

Edge proxy = ta, která jako první přijímá provoz z internetu. Musí být nastavená takto:

ProxySprávně (přepisuje)Špatně (appenduje)
nginxproxy_set_header X-Forwarded-For $remote_addr;proxy_add_x_forwarded_for
Apache mod_proxyRequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s" (před ProxyPass)výchozí chování mod_proxy_http
HAProxyoption forwardfor header X-Forwarded-For if-none + http-request del-header X-Forwarded-For před nímsamotné option forwardfor
TraefikforwardedHeaders.trustedIPs (mimo seznam se hlavička zahazuje)forwardedHeaders.insecure = true
Cloudflarepřepisuje automaticky (nebo použijte CF-Connecting-IP)-
Upozornění

Řetězíte-li víc proxy, tohle pravidlo platí jen pro tu nejkrajnější. Vnitřní hopy smí appendovat - musí ale být všechny uvedené v trusted_proxies, aby je aplikace uměla odloupnout.

Ověření (z internetu, ne z LAN):

curl -H 'X-Forwarded-For: 1.2.3.4' https://vas-server/api/health

V audit logu (Systém → Log) musí být vaše reálná IP, ne 1.2.3.4. Pokud vidíte 1.2.3.4, edge proxy hlavičku nepřepisuje a máte otevřený bypass.

Dodávaný Docker image

Image tenhle problém řeší i bez trusted_proxies: nginx uvnitř kontejneru předává PHP skutečnou IP TCP peera v parametru MYUCTO_CLIENT_IP (fastcgi_param bez prefixu HTTP_). Klientské hlavičky se do FastCGI vždy mapují jako HTTP_*, takže tenhle parametr nelze zvenčí podvrhnout a aplikace ho preferuje před X-Forwarded-For.

Když je ale před kontejnerem ještě další proxy, je „TCP peer" právě ona. Pak v docker/nginx.conf odkomentujte blok set_real_ip_from a vyjmenujte rozsahy té proxy - teprve tím se MYUCTO_CLIENT_IP přepočítá na reálného klienta:

set_real_ip_from  173.245.48.0/20;   # rozsahy vaší edge proxy
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

U IP omezení API tokenu aplikace zobrazuje IP aktuálního prohlížeče tak, jak ji vyhodnocuje při kontrole přístupu. U privátní, lokální nebo sdílené adresy upozorní na možné chybějící předávání IP. V interní síti může být tato adresa správná; integrace na jiném serveru může používat jinou IP než prohlížeč.

Upravený nginx konfigurák je nutné připojit také do služby aplikace v souboru Docker Compose (vedle ostatních volumes):

volumes:
  - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro

Použijte soubor odpovídající nasazenému image a po změně mountu znovu vytvořte kontejner aplikace. Do set_real_ip_from patří pouze skutečné důvěryhodné proxy, nikdy celý internet (0.0.0.0/0 nebo ::/0). Edge proxy musí příchozí klientské hlavičky bezpečně přepsat; postup ověření je výše.

101.11.5 RBAC (role-based access)

Klientské role mohou mít samostatná oprávnění Prohlížet banku, Nahrávat výpisy, Párovat platby a Ignorovat platby. Administrátor je přiděluje jednotlivě, ve výchozím stavu jsou vypnutá. Zapnutí akčního oprávnění zároveň zapne čtení banky; odebrání čtení odebere i bankovní akce. Pro párování a jeho zrušení musí mít klient také čtení všech dotčených faktur a zápis do jejich úhrad: invoices.mark_paid u vydaných, purchase_invoices.transition u přijatých. Samotné čtení dokladů nestačí. Nahrávání výpisů navíc vyžaduje zápis do obou oprávnění k úhradám, jinak by automatické párování při importu obešlo kontrolu ručního párování. Párování a import mohou spustit automatické účetní zpracování podle nastavení firmy; úhrady a účetní zápisy vznikají pod přihlášeným klientským uživatelem. Tato klientská omezení nemění oprávnění interních rolí.

Role se spravují v Systém → Role a oprávnění. Každý modul a významná akce mají jednu ze tří úrovní: neviditelné, pouze čtení nebo zápis. Zápis zahrnuje čtení; chybějící nebo neznámé oprávnění znamená zákaz.

Role typu staff jsou pro interní pracovníky. Role typu client mohou dostat jen katalogem povolené funkce klientského portálu. Předdefinované role Admin a Admin Plus stojí mimo editovatelnou matici a mají pevný plný přístup k přiděleným firmám. Admin Plus navíc zakládá firmy, ke kterým automaticky získá práva Admin. Superadmin má plný přístup ke všem firmám a jako jediný spravuje uživatele, role a globální administraci.

Každý non-superadmin potřebuje explicitní membership firmy. U jedné firmy může mít kompatibilní přepis role; role se nesčítají. Neaktivní role, neplatný přepis nebo prázdný membership jsou vždy fail-closed.

101.11.5.1 Jak je to vynucené

  1. Backend mapuje každou neveřejnou routu na konkrétní permission klíč a minimální úroveň. Nezmapovaná routa je odmítnuta; stavové, tenant a vlastnické guardy se kontrolují navíc.
  2. API token (PAT) má průnik oprávnění vlastníka pro aktuální firmu a scope tokenu. Scope read nikdy nepovolí zápis; odebrání firmy nebo snížení role se projeví existujícímu tokenu okamžitě.
  3. UI používá stejnou efektivní matici pro menu, přímé URL a skrytí akcí. Po přepnutí firmy stará práva zahodí a před vykreslením načte nová.

101.11.6 CSRF + Origin check

Každý mutating request (POST / PUT / PATCH / DELETE) musí mít:

  1. Origin header se shodující s přesným originem bezpečně rozpoznané domény
  2. X-CSRF-Token header se shodující s tokenem v session

Na canonical hostu je očekávaný origin odvozený z app.url; na aktivní vlastní doméně je to výhradně https://<její-hostname>. Jiný port, koncová tečka, podvržený Host, neaktivní alias ani origin jiné firmy neprojde.

Bez nich → 403 csrf_failed / origin_mismatch. UI to obsluhuje automaticky (token v Pinia store, header v axios interceptoru).

101.11.7 Activity log

Každá mutace (vytvoření / změna / vystavení / smazání) se loguje. Záznamy obsahují:

Viz 96. Nastavení pro UI.

101.11.7.1 Co log NEUKLÁDÁ

101.11.7.2 Jak se do logu zapisuje IP adresa

Aplikace bere IP klienta z IP síťového spojení (REMOTE_ADDR). Když běží za reverse proxy (Docker, nginx, Cloudflare…), je tím spojením proxy - bez konfigurace by se proto do auditu zapisovala IP proxy, ne reálného klienta (typicky uvidíte pořád stejnou IP, např. bránu Dockeru 172.x.0.1).

Reálnou IP přečte aplikace z hlavičky X-Forwarded-For pouze tehdy, když REMOTE_ADDR odpovídá rozsahu v cfg.ip_allowlist.trusted_proxies (viz § 101.11.4.1). Z hlavičky se bere první adresa (původní klient). Bez nastavené trusted_proxies se X-Forwarded-For ignoruje (ochrana proti podvržení).

Tip

Stejná logika se zjišťování IP používá i pro brute-force lockout (viz § 101.11.3). Za reverse proxy bez trusted_proxies proto lockout počítá pokusy podle IP proxy = fakticky globálně. Po nastavení trusted_proxies začnou audit log i lockout pracovat s reálnou klientskou IP.

101.11.8 DKIM podpis e-mailů

Pro deliverabilitu (aby gmail / o365 / seznam vaše maily nepoznačily jako spam) doporučujeme aktivovat DKIM:

  1. Vygenerujte RSA klíč: openssl genrsa -out private/dkim/myucto.pem 2048
  2. Public key → DNS TXT záznam myucto._domainkey.vase-domena.cz
  3. V cfg.php → smtp.dkim.enabled => true
  4. Restart služby

Detaily v README.md v rootu repa.

101.11.9 Klávesové zkratky

Položka Klávesové zkratky je pátým bodem menu pod jménem uživatele a zároveň pátou záložkou obrazovky Profil. Na mobilu je dostupná ve výběru záložek pod nadpisem Profil. Umožňuje změnit nebo vypnout zkratky pro viditelné položky hlavního menu, rychlé vytváření přes + a globální hledání. Preference se ukládá celosystémově k ID přihlášeného uživatele, nikoli k firmě nebo zařízení.

Formulář nedovolí duplicitní kombinace ani klávesy vyhrazené pro prohlížeč a pevné akce aplikace. Zkratky se nespouštějí při psaní do formuláře, během zamčené relace ani v otevřeném modálním dialogu. Obnovit výchozí odstraní uživatelský přepis a vrátí bezpečné kombinace popsané v Přehledu.

101.11.10 Anonymizovaná kopie pro testovací instanci

Pro ladění, školení nebo předání dat vývojáři se hodí kopie skutečné databáze, ve které ale nejsou osobní ani obchodní údaje. Vyrobí ji příkaz anonymize-clone:

# Linux / Docker
cmd/anonymize-clone.sh --from=myucto --to=myucto_anon
# Windows
cmd\anonymize-clone.ps1 --from=myucto --to=myucto_anon

Originální databáze se jen čte. Kopie vznikne jako nová databáze na témž serveru; do databáze, se kterou instance pracuje, zapsat nejde. Existující cíl se přepíše jen volbou --replace, a to jen tehdy, když je to předchozí anonymizovaná kopie.

Co se v kopii změní:

ÚdajNáhrada
názvy partnerů, jména osob, adresyvymyšlené, právní forma (s.r.o., a.s.) a rod příjmení zůstávají
IČO, DIČjiné, platné podle kontrolní číslice; DIČ navazuje na nové IČO
rodná čísladatum narození a pohlaví zůstávají, koncovka je jiná a číslo platné
čísla účtů, IBANjiná platná čísla, kód banky a předčíslí zůstávají
e-maily, telefony, datové schránkyvymyšlené (e-maily v doméně example.invalid)
texty dokladů, poznámky, protokoly převodů, auditní stopatatáž jména a čísla nahrazená týmiž pseudonymy
přílohy a výpisy uložené v databázizástupný soubor (prázdné PDF, obrázek 1×1 px, text)
hesla, API klíče, tokeny, certifikáty, šifrované archivy podánízneplatněné
relace, přihlašovací kódy, fronta odchozí pošty, licencevyprázdněné

Pseudonym je v rámci jednoho běhu stejný všude: partner má v kontaktu, na faktuře, v bankovní transakci i v textu úhrady tentýž nový název a IČO, takže párování plateb a výkazy fungují jako v originále. Částky, data, čísla dokladů a vazby se nemění - rozvaha, výsledovka i přiznání k DPH dávají stejná čísla. Veřejné účty institucí (finanční úřad, ČSSZ, pojišťovny, platební brány) zůstávají, aby aplikace dál poznala platby odvodů.

Klíč pseudonymizace je pro každý běh náhodný a nikam se neukládá, takže z kopie nejde originál dopočítat. Volba --seed=TEXT dá stejné pseudonymy i v dalším běhu, ale se známým seedem jde pseudonym zpětně dohledat - používejte ji jen na vývojovém stroji.

Po vytvoření kopie:

Soubory a dump:

Co se s kterým sloupcem stane, určuje seznam v api/src/Service/Anonymization/AnonymizationPolicy.php. Příkaz odmítne běžet, když databáze obsahuje textový sloupec, o kterém seznam nerozhoduje - nová funkce tak nemůže osobní údaje do kopie propašovat nepozorovaně. Na konci běhu příkaz vypíše sloupce, ve kterých zůstala hodnota shodná s originálem (typicky zachované účty institucí), aby šly zkontrolovat.

101.11.11 Tipy

Tip

Vypršení licence vaše data neohrozí. Bezplatné funkce původního MyInvoice zůstávají plně funkční včetně zápisu. Komerční moduly se skryjí i pro čtení a API, jejich data ale zůstávají beze změny ve vlastní databázi a po obnovení licence se znovu zpřístupní. Detail v 104. Licence a aktivace.

101.12 Související kapitoly