Quedaven dues preguntes de la regidoria de Vallmar sense respondre des del primer dia d'aquest mòdul, i cap de les dues no s'arregla amb un índex.
La primera la va fer la regidora de Cultura a la reunió de seguiment: "qui pot consultar els telèfons i els correus dels dotze mil socis?". Ningú no va saber respondre, que és la pitjor resposta possible. La segona la va fer l'interventor: "què passaria si el disc del servidor morís aquesta nit?". Algú va dir "tenim còpies", i en preguntar quan s'havia provat l'última restauració, es va fer el silenci.
Aquesta lliçó respon les dues, i tanca el mòdul i el bloc teòric del curs.
Hi ha una idea que convé posar per davant, perquè ordena tot el que segueix: la base de dades és l'última frontera. Si algú travessa el tallafoc, enganya l'aplicació o troba una credencial en un repositori, l'única cosa que queda entre aquella persona i les dades de dotze mil veïns de Vallmar són els permisos de la base de dades. I si l'edifici s'incendia, l'única cosa que queda entre la biblioteca i començar de zero són les còpies de seguretat —provades, no simplement existents—.
Veurem autenticació i autorització amb rols i GRANT; el principi de mínim privilegi aplicat a tres rols concrets de BiblioRed; vistes i seguretat a nivell de fila perquè cada sucursal vegi només el seu; la injecció SQL i per què les consultes parametritzades són l'única defensa que funciona; el xifratge en trànsit i en repòs; l'auditoria; el tractament de les dades personals; i el bloc complet de còpies de seguretat, on el registre WAL de 06-01 reapareix convertit en l'eina que permet recuperar la base de dades a l'instant anterior al desastre.
Contingut
- Per què la seguretat de la base de dades és diferent de la de l'aplicació
- Autenticació: rols, contrasenyes i
pg_hba.conf - Autorització:
GRANT,REVOKEi el catàleg de privilegis - Mínim privilegi: els tres rols de BiblioRed
- El perill de connectar-se com a superusuari
- Vistes per amagar columnes sensibles
- Seguretat a nivell de fila: cada sucursal veu el seu
- Injecció SQL: com es produeix i com s'impedeix
- Xifratge en trànsit, en repòs i el cas especial de les contrasenyes
- Auditoria: registre d'accessos i taula de canvis
- Dades personals: minimització, pseudonimització i retenció
- Còpies de seguretat: lògiques i físiques
- Completa, diferencial i incremental
- Arxivat del WAL i recuperació a un instant concret
- La regla 3-2-1 i la verificació de les còpies
- RPO i RTO aplicats a BiblioRed
- El pla de còpies de BiblioRed, comentat
- Els primers minuts després d'un esborrat accidental
- L'equivalent mínim a SQLite
- Per què la seguretat de la base de dades és diferent de la de l'aplicació
És habitual pensar que la seguretat es resol a la capa d'aplicació: si la pantalla de llistat de socis només la veu el personal autoritzat, les dades estan protegides. És una idea perillosa, i n'hi ha prou d'enumerar els camins que arriben a la base de dades sense passar per l'aplicació per veure-ho:
| Camí | Passa per la lògica de l'aplicació? |
|---|---|
Un desenvolupador amb psql des del seu portàtil |
No |
| Un script de manteniment nocturn | No |
| Una eina d'informes connectada per ODBC | No |
| Una còpia de seguretat copiada a un portàtil | No |
| Una injecció SQL a través del cercador del catàleg | Sí, però saltant-se la lògica |
| Un becari amb les credencials del fitxer de configuració | No |
Sis camins, i cinc no veuen ni una sola línia del codi de l'aplicació. D'aquí el principi:
Els controls d'accés han d'estar on són les dades. L'aplicació pot afegir comoditat i context, però la garantia ha de viure a la base de dades.
És exactament el mateix argument que hem fet servir a 04-04 per a les restriccions i a 06-02 per a l'aforament del club de lectura: una regla que no es pot trencar ha d'estar on no es pugui saltar. Aquí la regla és "el personal de taulell no pot exportar els correus dels socis".
Les capes de seguretat d'un sistema, de fora cap endins:
graph LR
A[Xarxa i tallafoc] --> B[TLS en transit]
B --> C[Autenticacio:<br/>qui ets?]
C --> D[Autoritzacio:<br/>que pots fer?]
D --> E[Seguretat de fila:<br/>sobre quines files?]
E --> F[Xifratge en repos]
F --> G[(Dades)]
D --> H[Auditoria:<br/>que has fet?]
Les dues preguntes centrals d'aquesta lliçó són la tercera i la quarta caixa: autenticació (qui ets?) i autorització (què pots fer?). Es confonen constantment i són coses diferents.
- Autenticació: rols, contrasenyes i
pg_hba.conf
pg_hba.confRols: una sola entitat per a usuaris i grups
PostgreSQL no distingeix entre "usuari" i "grup": té rols. Un rol amb l'atribut LOGIN es pot connectar; un rol sense ell serveix com a grup de permisos. CREATE USER és simplement una drecera de CREATE ROLE ... LOGIN.
-- Rol de grup: agrupa permisos, no es connecta
CREATE ROLE bibliored_taulell NOLOGIN;
-- Rol de connexió: una persona concreta
CREATE ROLE u_alsina LOGIN PASSWORD 'contrasenya-llarga-i-unica'
VALID UNTIL '2027-01-01';
-- Pertinença al grup
GRANT bibliored_taulell TO u_alsina;Atributs que convé conèixer:
| Atribut | Què concedeix |
|---|---|
LOGIN |
Es pot connectar |
SUPERUSER |
Se salta totes les comprovacions de permisos |
CREATEDB |
Pot crear bases de dades |
CREATEROLE |
Pot crear i modificar altres rols |
INHERIT (per omissió) |
Hereta automàticament els permisos dels rols als quals pertany |
NOINHERIT |
Els ha d'activar explícitament amb SET ROLE |
CONNECTION LIMIT n |
Màxim de connexions simultànies |
VALID UNTIL |
Data de caducitat de la contrasenya |
Inspeccionar el que hi ha:
List of roles
Role name | Attributes | Member of
---------------------+------------------------------------+---------------------
app_bibliored | | {bibliored_app}
bibliored_app | Cannot login | {}
bibliored_direccio | Cannot login | {}
bibliored_taulell | Cannot login | {}
postgres | Superuser, Create role, Create DB | {}
u_alsina | Password valid until 2027-01-01 | {bibliored_taulell}pg_hba.conf: qui es pot connectar des d'on i com
Abans de comprovar la contrasenya, PostgreSQL consulta el fitxer pg_hba.conf (host-based authentication). És una llista de regles que s'avalua de dalt a baix, i s'aplica la primera que encaixa. Si no n'encaixa cap, la connexió es rebutja.
# TIPUS BASE USUARI ADREÇA MÈTODE local all postgres peer host biblioredb bibliored_taulell 10.20.0.0/16 scram-sha-256 host biblioredb app_bibliored 10.20.5.11/32 scram-sha-256 hostssl biblioredb bibliored_direccio 0.0.0.0/0 scram-sha-256 host all all 0.0.0.0/0 reject
Els mètodes, amb la seva valoració:
| Mètode | Què fa | Usar-lo? |
|---|---|---|
scram-sha-256 |
Contrasenya amb desafiament-resposta; la contrasenya mai no viatja per la xarxa | Sí. És el mètode per omissió des de PostgreSQL 10 i el recomanat |
md5 |
Mètode antic, amb debilitats conegudes | Només per compatibilitat amb clients vells; migrar |
peer |
Comprova l'usuari del sistema operatiu (només connexions locals per sòcol) | Sí, per a tasques d'administració al mateix servidor |
cert |
Certificat de client TLS | Sí, en entorns amb gestió de certificats |
ldap, gss |
Delegació al directori corporatiu | Sí, en organitzacions amb directori |
trust |
Accepta qualsevol sense comprovar res | NO |
reject |
Denega sempre | Sí, com a regla final |
Sobre
trust. Significa literalment "qualsevol que arribi per aquesta via entra com l'usuari que digui ser, sense contrasenya". El seu únic ús legítim és una instància local de desenvolupament al teu propi equip, sense dades reals i sense port obert a l'exterior. Una líniahost all all 0.0.0.0/0 trusten un servidor equival a publicar la base de dades a internet sense porta. Apareix amb més freqüència de la que ningú voldria admetre, gairebé sempre perquè algú la va posar "un moment, per provar" i ningú no la va treure.
Fixa't en la línia hostssl per a direcció: obliga que la connexió vingui xifrada. I l'última línia, reject, converteix la llista en una política de denegar per omissió, que és com s'han d'escriure totes les llistes de control d'accés.
Després d'editar el fitxer:
sudo systemctl reload postgresql
# o, sense permisos de sistema, des de psql com a superusuari:
# SELECT pg_reload_conf();Comprovar quines regles estan actives:
line_number | type | database | user_name | address | auth_method
-------------+---------+---------------+----------------------+-------------+---------------
80 | local | {all} | {postgres} | | peer
81 | host | {biblioredb} | {bibliored_taulell} | 10.20.0.0 | scram-sha-256
82 | host | {biblioredb} | {app_bibliored} | 10.20.5.11 | scram-sha-256
83 | hostssl | {biblioredb} | {bibliored_direccio} | 0.0.0.0 | scram-sha-256
84 | host | {all} | {all} | 0.0.0.0 | reject
- Autorització:
GRANT, REVOKE i el catàleg de privilegis
GRANT, REVOKE i el catàleg de privilegisAutenticat el rol, la pregunta següent és què pot fer. El model de PostgreSQL és jeràrquic: per arribar a una taula cal travessar la base de dades i l'esquema.
graph TD
A["Base de dades<br/>privilegi CONNECT"] --> B["Esquema<br/>privilegi USAGE"]
B --> C["Taula<br/>SELECT / INSERT / UPDATE / DELETE"]
B --> D["Sequencia<br/>USAGE / SELECT"]
B --> E["Funcio<br/>EXECUTE"]
C --> F["Columna<br/>SELECT (col) / UPDATE (col)"]
És l'error número u en configurar permisos: donar SELECT sobre les taules i oblidar el USAGE sobre l'esquema. Sense USAGE, el rol no pot ni veure que la taula existeix.
Catàleg de privilegis
| Privilegi | S'aplica a | Permet |
|---|---|---|
CONNECT |
Base de dades | Connectar-s'hi |
CREATE |
Base de dades, esquema | Crear esquemes / objectes |
USAGE |
Esquema, seqüència, tipus | Accedir als objectes de l'esquema; usar la seqüència |
SELECT |
Taula, vista, columna | Llegir |
INSERT |
Taula, columna | Inserir |
UPDATE |
Taula, columna | Modificar |
DELETE |
Taula | Esborrar files |
TRUNCATE |
Taula | Buidar la taula |
REFERENCES |
Taula, columna | Crear claus foranes que l'apuntin |
TRIGGER |
Taula | Crear activadors |
EXECUTE |
Funció, procediment | Executar-la |
Sintaxi
GRANT SELECT, INSERT ON prestecs TO bibliored_taulell;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO bibliored_direccio;
GRANT USAGE ON SCHEMA public TO bibliored_taulell;
REVOKE DELETE ON prestecs FROM bibliored_taulell;Dues formes avançades imprescindibles:
Privilegis a nivell de columna. Es pot autoritzar la lectura d'unes columnes i no d'unes altres:
Aquell rol podrà llegir el nom d'un soci però no el seu email. Si ho intenta:
Privilegis per omissió per a objectes futurs. Un GRANT ON ALL TABLES afecta les taules que existeixen avui. La taula que algú creï demà no hi estarà inclosa, i aquesta és una fallada silenciosa clàssica:
El problema del rol PUBLIC
PostgreSQL té un pseudo-rol anomenat PUBLIC al qual pertanyen tots. Històricament, PUBLIC tenia CREATE sobre l'esquema public, cosa que permetia a qualsevol usuari crear-hi taules. Des de PostgreSQL 15 ja no és així, però en instal·lacions més antigues —o migrades— convé comprovar-ho i corregir-ho:
Inspeccionar els permisos concedits:
Schema | Name | Type | Access privileges
--------+-------+-------+---------------------------------------------------------
public | socis | table | bibliored_app=arwd/postgres +
| | | bibliored_direccio=r/postgres +
| | | bibliored_taulell=r(soci_id,nom,cognoms)/postgresLes lletres: r = SELECT, a = INSERT, w = UPDATE, d = DELETE, x = REFERENCES, U = USAGE.
- Mínim privilegi: els tres rols de BiblioRed
Principi de mínim privilegi. Cada rol rep exactament els permisos que necessita per a la seva funció, ni un més, i només mentre els necessita.
BiblioRed necessita tres perfils. Els definim primer en llenguatge planer, perquè un permís que no es pot explicar en una frase sol estar mal pensat:
| Rol | Qui és | Què necessita | Què NO ha de poder fer |
|---|---|---|---|
bibliored_taulell |
Personal de les quatre sucursals | Prestar, retornar, donar d'alta socis, cobrar multes, inscriure a esdeveniments | Llegir correus i telèfons de socis; esborrar res; veure altres sucursals |
bibliored_direccio |
Direcció i regidoria, per a informes | Llegir tot allò agregat i estadístic | Escriure absolutament res; llegir dades de contacte |
bibliored_app |
L'aplicació web pública | Consultar el catàleg, gestionar reserves i inscripcions del soci autenticat | Tocar multes, pagaments, socis complets, ni cap dada d'un altre soci |
L'script complet
-- =========================================================
-- 1) Tancament per omissió: ningú no té res que no se li doni
-- =========================================================
REVOKE ALL ON DATABASE biblioredb FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM PUBLIC;
-- =========================================================
-- 2) Rols de grup (sense LOGIN: són contenidors de permisos)
-- =========================================================
CREATE ROLE bibliored_taulell NOLOGIN;
CREATE ROLE bibliored_direccio NOLOGIN;
CREATE ROLE bibliored_app NOLOGIN;
GRANT CONNECT ON DATABASE biblioredb
TO bibliored_taulell, bibliored_direccio, bibliored_app;
GRANT USAGE ON SCHEMA public
TO bibliored_taulell, bibliored_direccio, bibliored_app;
-- =========================================================
-- 3) TAULELL: opera el dia a dia, sense dades de contacte
-- =========================================================
GRANT SELECT, INSERT, UPDATE ON prestecs, reserves, inscripcions
TO bibliored_taulell;
GRANT SELECT, UPDATE (estat, sucursal_id) ON exemplars
TO bibliored_taulell;
GRANT SELECT ON materials, materials_llibre, materials_dvd,
materials_revista, materials_audiollibre, subtitols_dvd,
autors, sucursals, sales, tipus_esdeveniment, esdeveniments, llibres
TO bibliored_taulell;
-- Socis: alta i modificació, però SENSE llegir email
GRANT SELECT (soci_id, nom, cognoms, data_alta, sucursal_id, actiu)
ON socis TO bibliored_taulell;
GRANT INSERT ON socis TO bibliored_taulell;
GRANT UPDATE (nom, cognoms, email, sucursal_id, actiu)
ON socis TO bibliored_taulell;
-- Multes i pagaments: emetre i cobrar, mai esborrar
GRANT SELECT, INSERT, UPDATE ON multes TO bibliored_taulell;
GRANT SELECT, INSERT ON pagaments TO bibliored_taulell;
-- Seqüències: sense USAGE no es pot inserir en taules amb identitat
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO bibliored_taulell;
-- =========================================================
-- 4) DIRECCIÓ: només lectura, i sense dades de contacte
-- =========================================================
GRANT SELECT ON ALL TABLES IN SCHEMA public TO bibliored_direccio;
-- Es retira l'accés al que és sensible i se substitueix per la vista de l'apartat 6
REVOKE SELECT ON socis, telefons_soci FROM bibliored_direccio;
GRANT SELECT ON v_socis_public TO bibliored_direccio;
-- Objectes futurs: que no s'escapin per oblit
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO bibliored_direccio;
-- =========================================================
-- 5) APLICACIÓ WEB: superfície mínima
-- =========================================================
GRANT SELECT ON materials, materials_llibre, materials_dvd,
materials_revista, materials_audiollibre, subtitols_dvd,
autors, sucursals, exemplars, sales, tipus_esdeveniment, llibres
TO bibliored_app;
GRANT SELECT ON v_esdeveniments_publicats TO bibliored_app;
GRANT SELECT, INSERT, UPDATE ON reserves, inscripcions TO bibliored_app;
GRANT SELECT ON prestecs TO bibliored_app;
GRANT SELECT (soci_id, nom, cognoms, email, sucursal_id, actiu)
ON socis TO bibliored_app;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO bibliored_app;
-- El que l'aplicació NO pot tocar, dit explícitament
REVOKE ALL ON multes, pagaments, informes_esdeveniment FROM bibliored_app;
-- =========================================================
-- 6) Rols de connexió reals
-- =========================================================
CREATE ROLE u_alsina LOGIN PASSWORD 'xxxxxxxxxxxx' VALID UNTIL '2027-01-01';
CREATE ROLE u_pereda LOGIN PASSWORD 'xxxxxxxxxxxx' VALID UNTIL '2027-01-01';
CREATE ROLE u_direccio LOGIN PASSWORD 'xxxxxxxxxxxx' VALID UNTIL '2027-01-01';
CREATE ROLE app_bibliored LOGIN PASSWORD 'xxxxxxxxxxxx' CONNECTION LIMIT 40;
GRANT bibliored_taulell TO u_alsina, u_pereda;
GRANT bibliored_direccio TO u_direccio;
GRANT bibliored_app TO app_bibliored;Comprovar-ho
Un permís que no s'ha provat és una suposició. La comprovació es fa suplantant el rol:
SET ROLE bibliored_taulell;
SELECT nom, cognoms FROM socis WHERE soci_id = 14; -- ha de funcionar
SELECT email FROM socis WHERE soci_id = 14; -- ha de fallar
DELETE FROM prestecs WHERE prestec_id = 88301; -- ha de fallar
RESET ROLE;nom | cognoms --------+------------ Marta | Alsina ERROR: permission denied for table socis ERROR: permission denied for table prestecs
Dos errors esperats i una consulta correcta: els permisos fan el que diu el paper.
Existeix a més una funció per consultar-ho sense executar res:
SELECT has_table_privilege('bibliored_taulell', 'multes', 'DELETE') AS pot_esborrar_multes,
has_column_privilege('bibliored_taulell', 'socis', 'email', 'SELECT') AS pot_llegir_email;
- El perill de connectar-se com a superusuari
És la mala pràctica més estesa i la més fàcil de corregir.
Quan una aplicació es connecta amb un rol superusuari —postgres a la majoria d'instal·lacions—, tota la feina dels apartats anteriors queda anul·lada. Un superusuari se salta totes les comprovacions de permisos, inclosa la seguretat a nivell de fila de l'apartat 7.
Què pot fer un atacant que aconsegueixi la connexió de l'aplicació:
Amb bibliored_app |
Amb postgres (superusuari) |
|---|---|
| Llegir el catàleg i les reserves | Llegir-ho tot, incloses multes i pagaments |
| No pot esborrar taules | DROP TABLE prestecs; |
No pot tocar pg_hba.conf |
Pot crear-se usuaris i donar-se accés permanent |
| No pot llegir fitxers del servidor | COPY ... FROM PROGRAM executa ordres del sistema operatiu |
Aquella última fila converteix una injecció SQL de gravetat mitjana en un compromís total del servidor.
La llista de comprovació:
- La cadena de connexió de l'aplicació mai no usa
postgresni cap rol ambSUPERUSER. - El propietari de les taules és un rol d'administració diferent del que usa l'aplicació.
- Les contrasenyes viuen en un gestor de secrets o en variables d'entorn del servei, mai al repositori de codi.
- Cada persona té el seu propi rol de connexió. Un compte compartit fa impossible l'auditoria de l'apartat 10.
Verificar que no hi ha superusuaris de més:
Un. Si n'apareixen tres o quatre, hi ha feina a fer.
- Vistes per amagar columnes sensibles
Els privilegis de columna de l'apartat 3 funcionen bé, però tenen un inconvenient pràctic: SELECT * falla, i moltes eines d'informes l'usen. L'alternativa elegant és una vista.
Una vista s'executa amb els permisos de qui la va crear, no de qui la consulta. Això permet donar accés a un subconjunt de dades sense donar accés a la taula subjacent.
CREATE VIEW v_socis_public AS
SELECT soci_id,
nom,
cognoms,
data_alta,
sucursal_id,
actiu
FROM socis;
-- El rol NO té permís sobre socis, però sí sobre la vista
REVOKE ALL ON socis FROM bibliored_direccio;
GRANT SELECT ON v_socis_public TO bibliored_direccio;Comprovació:
soci_id | nom | cognoms | data_alta | sucursal_id | actiu
---------+-------+-------------+------------+-------------+-------
14 | Marta | Alsina | 2019-03-11 | 1 | t
15 | Ivan | Pereda | 2021-09-02 | 2 | t
16 | Nuria | Bastos | 2023-01-24 | 3 | tSELECT * funciona, i els correus i telèfons són inabastables.
Altres vistes útils del mateix tipus a BiblioRed:
-- Catàleg públic d'esdeveniments: sense notes internes ni informes
CREATE VIEW v_esdeveniments_publicats AS
SELECT e.esdeveniment_id, e.titol, t.nom AS tipus, s.nom AS sala,
e.inici, e.fi, e.places_ofertes
FROM esdeveniments e
JOIN tipus_esdeveniment t ON t.tipus_esdeveniment_id = e.tipus_esdeveniment_id
JOIN sales s ON s.sala_id = e.sala_id
WHERE e.publicat AND e.estat = 'programat';
-- Dades de contacte emmascarades, per a suport tècnic
CREATE VIEW v_socis_contacte_emmascarat AS
SELECT soci_id,
nom,
cognoms,
regexp_replace(email, '(.).*(@.*)', '\1***\2') AS email_emmascarat,
sucursal_id
FROM socis; soci_id | nom | cognoms | email_emmascarat | sucursal_id
---------+-------+---------+--------------------+-------------
14 | Marta | Alsina | m***@example.org | 1Un advertiment sobre l'emmascarament: serveix perquè el personal de suport pugui verificar un correu que el soci dicta per telèfon, no per anonimitzar. Un emmascarament no és una anonimització, i a l'apartat 11 veurem la diferència.
Nota tècnica: per omissió les vistes són SECURITY INVOKER quant a la seguretat a nivell de fila de l'apartat següent, però s'executen amb els permisos del propietari respecte a les taules. Si necessites que la vista apliqui les polítiques de fila de l'usuari que consulta, declara-les amb WITH (security_invoker = true), disponible des de PostgreSQL 15.
- Seguretat a nivell de fila: cada sucursal veu el seu
Les vistes amaguen columnes. Per amagar files —que el personal de la sucursal Nord no vegi els préstecs de la sucursal Sud— PostgreSQL ofereix la seguretat a nivell de fila (row level security, RLS).
Amb RLS, cada taula pot portar polítiques que actuen com un
WHEREimplícit i obligatori, aplicat pel gestor a tota consulta dels rols afectats.
Pas 1: activar RLS
Atenció: amb RLS activada i sense cap política, ningú no veu res. El comportament per omissió és denegar, que és el correcte.
Pas 2: establir el context de la sessió
La política necessita saber a quina sucursal és l'usuari. L'aplicació ho comunica amb un paràmetre de sessió en obrir la connexió:
Pas 3: crear les polítiques
-- El personal de taulell només veu els préstecs de la seva sucursal
CREATE POLICY pol_prestecs_sucursal ON prestecs
FOR ALL
TO bibliored_taulell
USING (
EXISTS (
SELECT 1 FROM exemplars e
WHERE e.exemplar_id = prestecs.exemplar_id
AND e.sucursal_id = current_setting('app.sucursal_actual')::int
)
);
-- Direcció veu tots els préstecs, sense restricció de fila
CREATE POLICY pol_prestecs_direccio ON prestecs
FOR SELECT
TO bibliored_direccio
USING (true);Les clàusules d'una política:
| Clàusula | Què controla |
|---|---|
USING (expr) |
Quines files són visibles (SELECT, UPDATE, DELETE) |
WITH CHECK (expr) |
Quines files es poden crear o deixar (INSERT, UPDATE) |
FOR |
A quines operacions s'aplica (ALL, SELECT, INSERT, UPDATE, DELETE) |
TO |
A quins rols |
Sense WITH CHECK, un rol podria inserir files que després no pot veure, cosa que sol ser un error. La política completa per a socis:
ALTER TABLE socis ENABLE ROW LEVEL SECURITY;
CREATE POLICY pol_socis_sucursal ON socis
FOR ALL
TO bibliored_taulell
USING (sucursal_id = current_setting('app.sucursal_actual')::int)
WITH CHECK (sucursal_id = current_setting('app.sucursal_actual')::int);Comprovació
La mateixa consulta, el mateix rol, resultats diferents: el gestor està aplicant el filtre pel seu compte. I és impossible saltar-se'l des de SQL:
Els tres advertiments sobre RLS
- Els superusuaris i els propietaris de la taula se salten RLS. Si l'aplicació es connecta com a propietària de les taules, les polítiques no s'apliquen. Cal forçar-ho amb
ALTER TABLE socis FORCE ROW LEVEL SECURITY;. - Té cost de rendiment. La política es converteix en una condició afegida a cada consulta, i les polítiques amb subconsultes com la de
prestecspoden canviar el pla d'execució. Comprova-ho ambEXPLAIN ANALYZE(06-03). - El paràmetre de sessió l'ha de fixar l'aplicació de manera fiable i no l'ha de poder alterar l'usuari final. Si l'usuari pot influir en
app.sucursal_actual, la política no protegeix de res.
- Injecció SQL: com es produeix i com s'impedeix
És la vulnerabilitat més antiga i més coneguda de les aplicacions amb base de dades, i continua apareixent cada any a les llistes d'incidents. La seva causa és sempre la mateixa: barrejar codi i dades a la mateixa cadena de text.
Com es produeix
El cercador del catàleg de BiblioRed construeix la consulta concatenant el que l'usuari escriu:
# CODI VULNERABLE — no escriguis això mai
terme = request.args.get("q")
sql = "SELECT material_id, titol FROM materials WHERE titol LIKE '%" + terme + "%'"
cursor.execute(sql)Amb una cerca normal, q = mapa, la consulta resultant és correcta:
Ara un visitant escriu al cercador:
La consulta que arriba al servidor és:
SELECT material_id, titol FROM materials WHERE titol LIKE '%' UNION SELECT soci_id, email FROM socis --%' material_id | titol
-------------+------------------------------
907 | El mapa del temps
14 | [email protected]
15 | [email protected]
16 | [email protected]
...Els dotze mil correus dels socis de Vallmar, al cercador públic del catàleg. Sense contrasenya, sense eines i sense deixar més rastre que una línia al registre de consultes.
I això és només la lectura. El que permet una injecció, en general:
| Objectiu | Exemple |
|---|---|
| Llegir qualsevol taula accessible | UNION SELECT sobre socis, multes, pagaments |
| Modificar o esborrar dades | '; UPDATE multes SET estat='pagada'; -- |
| Eludir l'autenticació | ' OR '1'='1 en un formulari d'accés |
| Extreure l'esquema | Consultes a information_schema |
| Denegar el servei | pg_sleep(60) a cada petició |
| Executar ordres del sistema | COPY ... FROM PROGRAM, només si la connexió és superusuari |
Aquella última fila explica per què els apartats 4 i 5 són part de la defensa contra la injecció: amb bibliored_app ben limitat, la mateixa injecció no pot llegir multes, ni esborrar res, ni tocar el sistema operatiu. El dany d'una injecció és exactament igual als permisos de la connexió.
La defensa principal: consultes parametritzades
Una consulta parametritzada envia el text SQL i els valors per camins separats. El gestor rep primer l'estructura de la consulta i després les dades, així que és impossible que una dada s'interpreti com a codi. No hi ha res a escapar perquè no hi ha res a barrejar.
# CODI CORRECTE
terme = request.args.get("q")
sql = "SELECT material_id, titol FROM materials WHERE titol ILIKE %s"
cursor.execute(sql, ('%' + terme + '%',))Amb el mateix atac, el resultat és:
La base de dades ha buscat, literalment, materials el títol dels quals contingui la cadena ' UNION SELECT soci_id, email FROM socis --. No n'hi ha cap. L'atac s'ha convertit en una cerca sense resultats, que és exactament el que ha de ser.
En SQL directe, dins de psql o d'una funció, l'equivalent és PREPARE:
PREPARE cercar_material (text) AS
SELECT material_id, titol FROM materials WHERE titol ILIKE '%' || $1 || '%';
EXECUTE cercar_material ('mapa');Per què escapar a mà no basta
És temptador pensar que n'hi ha prou de "treure les cometes". No n'hi ha prou, per cinc raons:
| Problema | Explicació |
|---|---|
| Cal recordar-ho sempre | N'hi ha prou amb un lloc oblidat de dos-cents perquè la defensa no existeixi |
| Depèn de la codificació | Certes codificacions multibyte permeten construir seqüències que sobreviuen a l'escapament |
| Els números no porten cometes | WHERE soci_id = + entrada no es protegeix escapant cometes |
| No cobreix els identificadors | Un nom de columna o de taula dinàmic necessita un altre tractament (quote_ident) |
| És codi propi al camí crític | Qualsevol error a la funció d'escapament obre el forat sencer |
Una consulta parametritzada no té cap d'aquests problemes perquè no escapa res: separa els camins.
Quan el SQL ha de ser dinàmic
De vegades la part variable és el nom d'una columna d'ordenació, i això no es pot parametritzar. La solució no és escapar, és validar contra una llista blanca:
COLUMNES_ORDRE = {"titol": "titol", "any": "any_publicacio", "autor": "autor_id"}
col = COLUMNES_ORDRE.get(request.args.get("ordre"), "titol") # si no hi és, valor per omissió
sql = f"SELECT material_id, titol FROM materials ORDER BY {col} LIMIT %s"
cursor.execute(sql, (20,))L'entrada de l'usuari mai no arriba al SQL: només s'usa com a clau per triar entre valors fixos escrits per tu.
Defenses complementàries
Cap no substitueix la parametrització; totes redueixen el dany:
- Mínim privilegi (apartat 4): que la connexió no pugui llegir el que no li toca.
- Validació d'entrada: comprovar que un identificador és un enter, que una data és una data.
- Mai no mostris el missatge d'error de la base de dades a l'usuari final. Un
ERROR: column "socis.email" does not existés un mapa de l'esquema regalat. - Registrar els errors de SQL: una ràfega d'errors de sintaxi des d'una mateixa adreça és un atac en curs.
- Xifratge en trànsit, en repòs i el cas especial de les contrasenyes
En trànsit: TLS
Sense xifratge, les consultes i els seus resultats viatgen en clar per la xarxa. Qualsevol amb accés al tram pot llegir els correus dels socis a mesura que es transmeten.
# postgresql.conf ssl = on ssl_cert_file = '/etc/ssl/certs/biblioredb.crt' ssl_key_file = '/etc/ssl/private/biblioredb.key'
I a pg_hba.conf, hostssl en lloc de host per obligar que la connexió vingui xifrada. Del costat del client:
Els modes de sslmode, que importen més del que sembla:
| Mode | Xifra | Verifica el certificat | Verifica el nom del servidor |
|---|---|---|---|
disable |
No | — | — |
require |
Sí | No | No |
verify-ca |
Sí | Sí | No |
verify-full |
Sí | Sí | Sí |
require xifra però no comprova amb qui parla, així que no protegeix d'un intermediari. La configuració correcta en producció és verify-full.
Comprovar l'estat d'una connexió:
ssl | version | cipher -----+---------+------------------------- t | TLSv1.3 | TLS_AES_256_GCM_SHA384
En repòs
Dos nivells, amb propòsits diferents:
| Nivell | Com | Protegeix de | No protegeix de |
|---|---|---|---|
| Disc / volum (LUKS, xifratge del proveïdor) | Transparent per a PostgreSQL | Robatori físic del disc, retirada de maquinari, còpies en suports extraviats | Res del que passi amb el servidor encès |
Columna (pgcrypto) |
Xifratge explícit de valors concrets | Lectura directa dels fitxers o d'una còpia | Requereix gestionar claus, i impedeix indexar i buscar per aquella columna |
El xifratge de disc és barat, transparent i ha d'estar activat sempre. El xifratge per columna és una eina quirúrgica: a BiblioRed no hi ha res que ho justifiqui —no es desen números de targeta ni dades de salut—, i aplicar-lo a email faria impossible buscar per correu i obligaria a custodiar una clau la pèrdua de la qual equivaldria a perdre les dades.
Regla: xifra el disc sempre; xifra columnes només quan puguis anomenar la dada exacta, el risc exacte i qui custodia la clau.
Contrasenyes: l'excepció que no es xifra
Les contrasenyes dels socis per al portal web mereixen un apartat propi perquè l'error aquí és greu i freqüent.
| Pràctica | Veredicte |
|---|---|
| Desar la contrasenya en clar | Inacceptable |
| Desar-la xifrada de manera reversible | Inacceptable: qui tingui la clau les té totes |
Desar un MD5 o SHA-1 de la contrasenya |
Inacceptable: es trenquen amb taules precalculades |
Desar un SHA-256 sense sal |
Insuficient: ràpid de provar per força bruta |
| Desar el resultat d'una funció de derivació de clau amb sal (bcrypt, scrypt, Argon2) | Correcte |
Una contrasenya no es xifra: es transforma amb una funció d'un sol sentit, deliberadament lenta i amb sal. No cal poder-la recuperar; només cal poder-la comprovar. Per això els sistemes seriosos ofereixen "restablir la contrasenya" i mai "recordar-li la seva contrasenya".
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- En donar d'alta o canviar la contrasenya
UPDATE socis
SET clau_hash = crypt('la-contrasenya-del-soci', gen_salt('bf', 12))
WHERE soci_id = 14;
-- En comprovar-la
SELECT soci_id
FROM socis
WHERE soci_id = 14
AND clau_hash = crypt('la-contrasenya-teclejada', clau_hash);Si la contrasenya és incorrecta, la consulta torna zero files. El paràmetre 12 de gen_salt('bf', 12) és el cost: cada unitat duplica el temps de càlcul, cosa que alenteix els atacs per força bruta sense molestar l'usuari legítim.
Nota important: l'anterior val per a les contrasenyes dels socis a l'aplicació. Les contrasenyes dels rols de PostgreSQL les gestiona el mateix servidor amb SCRAM-SHA-256, i no cal fer res especial més enllà d'usar aquell mètode a pg_hba.conf.
- Auditoria: registre d'accessos i taula de canvis
Autenticació, autorització i xifratge responen a "qui pot?". L'auditoria respon a "qui ha fet què, i quan?", que és la pregunta que es fa després d'un incident, i també la que exigeix qualsevol revisió seriosa de tractament de dades personals.
Registre del servidor
# postgresql.conf log_connections = on log_disconnections = on log_statement = 'ddl' # 'none' | 'ddl' | 'mod' | 'all' log_min_duration_statement = 1000 log_line_prefix = '%m [%p] %u@%d de %h '
| Paràmetre | Què registra | Cost |
|---|---|---|
log_connections / log_disconnections |
Qui es connecta i des d'on | Molt baix |
log_statement = 'ddl' |
Canvis d'esquema | Baix. Mínim recomanable |
log_statement = 'mod' |
A més, totes les escriptures | Mitjà |
log_statement = 'all' |
Absolutament tot | Alt, i registra dades personals en text pla |
Aquell últim punt mereix atenció: activar log_statement = 'all' en una base amb dades personals significa que els correus i telèfons dels socis acaben escrits als fitxers de registre, que sovint tenen menys protecció i més còpies que la mateixa base de dades. És un cas clar d'una mesura de seguretat que crea un problema de privacitat.
Taula d'auditoria amb activadors
Per saber qui va modificar quina fila i quan, el patró estàndard és una taula d'auditoria alimentada per un activador —els TRIGGER que vam presentar a 05-04—.
CREATE TABLE auditoria_socis (
auditoria_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
soci_id INTEGER NOT NULL,
operacio TEXT NOT NULL CHECK (operacio IN ('INSERT','UPDATE','DELETE')),
moment TIMESTAMPTZ NOT NULL DEFAULT now(),
usuari_bd TEXT NOT NULL DEFAULT current_user,
adreca_ip INET,
dades_abans JSONB,
dades_despres JSONB
);
CREATE INDEX idx_auditoria_socis_soci ON auditoria_socis (soci_id, moment DESC);
CREATE FUNCTION fn_auditar_socis() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO auditoria_socis (soci_id, operacio, adreca_ip, dades_abans, dades_despres)
VALUES (
coalesce(NEW.soci_id, OLD.soci_id),
TG_OP,
inet_client_addr(),
CASE WHEN TG_OP IN ('UPDATE','DELETE') THEN to_jsonb(OLD) END,
CASE WHEN TG_OP IN ('INSERT','UPDATE') THEN to_jsonb(NEW) END
);
RETURN NULL; -- activador AFTER: el valor tornat s'ignora
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trg_auditar_socis
AFTER INSERT OR UPDATE OR DELETE ON socis
FOR EACH ROW EXECUTE FUNCTION fn_auditar_socis();Provem-ho:
UPDATE socis SET email = '[email protected]' WHERE soci_id = 14;
SELECT operacio, moment, usuari_bd,
dades_abans ->> 'email' AS email_abans,
dades_despres ->> 'email' AS email_despres
FROM auditoria_socis
WHERE soci_id = 14
ORDER BY moment DESC LIMIT 1;operacio | moment | usuari_bd | email_abans | email_despres ----------+-------------------------------+------------+--------------------------+------------------------------- UPDATE | 2026-08-02 13:14:52.118+02 | u_alsina | [email protected] | [email protected]
Quatre decisions de disseny que convé entendre:
SECURITY DEFINERfa que la funció s'executi amb els permisos de qui la va crear, de manera que el rol de taulell pot generar registres d'auditoria sense tenir permís d'escriptura sobre la taula d'auditoria. És just el que es vol: poder escriure al registre però no poder-lo esborrar.current_useridentifica el rol de la base de dades. Per això importa que cada persona tingui el seu propi rol de connexió (apartat 5): amb un compte compartit, aquesta columna no diu res útil.JSONBper a l'abans i el després evita haver de refer la taula d'auditoria cada vegada que canvia l'esquema desocis. És un ús legítim dejsonbdels que 04-04 anomenava "escapatòries controlades".- El cost: l'auditoria duplica les escriptures i fa créixer una taula que ningú no consulta cada dia. S'aplica a les taules amb dades sensibles —
socis,multes,pagaments—, no a totes.
I un advertiment final: la taula d'auditoria conté també dades personals, incloses les que s'hagin esborrat de la taula original. Necessita els mateixos permisos restrictius i la mateixa política de retenció que les dades que audita.
- Dades personals: minimització, pseudonimització i retenció
⚠️ Advertiment important. El que segueix descriu mecanismes tècnics per tractar dades personals en una base de dades. No és assessorament legal. El compliment del Reglament General de Protecció de Dades (RGPD) i de la legislació aplicable a cada jurisdicció —incloses les obligacions específiques d'una administració pública com l'ajuntament de Vallmar— l'ha de revisar un professional de compliance o el delegat de protecció de dades de l'organització. Les decisions sobre quines dades es poden recollir, amb quina base jurídica, durant quant de temps i amb quines mesures, són decisions jurídiques i organitzatives, no tècniques. Aquesta lliçó ensenya com implementar el que es decideixi, no què s'ha de decidir.
Dit això, hi ha quatre tècniques que tot professional de bases de dades hauria de conèixer.
Minimització
La mesura més eficaç és no tenir la dada.
Repassant l'esquema de BiblioRed amb aquella pregunta:
| Dada | Per a què s'usa? | Decisió |
|---|---|---|
socis.email |
Avisos de venciment i reserves disponibles | Es conserva: hi ha una funció clara |
telefons_soci |
Trucades per retards greus | Es conserva, revisant si en calen diversos per soci |
| Data de naixement completa | Només per saber si és carnet infantil o adult | Substituïble per l'any, o per una bandera es_menor |
| Adreça postal completa | Res al sistema actual | Eliminar |
| DNI | Verificació a l'alta presencial | Verificar i no emmagatzemar, o emmagatzemar només una comprovació |
Cada dada que no hi és no es pot filtrar, no s'ha de xifrar, no s'ha d'auditar i no s'ha d'esborrar.
Pseudonimització
Pseudonimitzar és substituir els identificadors directes per una referència que no identifica per si sola, conservant en un altre lloc i amb altres proteccions la correspondència. És reversible amb la informació addicional.
A BiblioRed, la taula d'estadístiques d'ús no necessita saber qui és cada soci:
CREATE TABLE estadistiques_us (
pseudonim TEXT NOT NULL,
data DATE NOT NULL,
sucursal_id INTEGER NOT NULL REFERENCES sucursals(sucursal_id),
tipus_material TEXT NOT NULL,
prestecs INTEGER NOT NULL
);
-- El pseudònim és estable (permet sèries temporals) i no invertible sense la sal
INSERT INTO estadistiques_us (pseudonim, data, sucursal_id, tipus_material, prestecs)
SELECT encode(digest(p.soci_id::text || current_setting('app.sal_estadistica'), 'sha256'), 'hex'),
p.data_prestec, e.sucursal_id, m.tipus, count(*)
FROM prestecs p
JOIN exemplars e ON e.exemplar_id = p.exemplar_id
JOIN materials m ON m.material_id = e.material_id
GROUP BY 1, 2, 3, 4;La clau és a la sal (app.sal_estadistica): sense ella, un sha256 d'un identificador numèric s'inverteix provant els 12.000 valors possibles en menys d'un segon. Un pseudònim sense sal no és un pseudònim.
Anonimització per a entorns de prova
Anonimitzar és transformar les dades de manera que ja no sigui possible identificar la persona, ni tan sols amb informació addicional. A diferència de la pseudonimització, és irreversible.
El cas pràctic és l'entorn de desenvolupament. Copiar la base de producció al portàtil d'un desenvolupador —que és el que es fa a la meitat de les organitzacions— significa distribuir les dades de dotze mil veïns per equips sense control, sense xifratge de disc garantit i sense auditoria.
Un guió d'anonimització per a l'entorn de proves de BiblioRed:
-- EXECUTAR NOMÉS SOBRE LA CÒPIA DE PROVES. Mai sobre producció.
BEGIN;
UPDATE socis SET
nom = 'Soci' || soci_id,
cognoms = 'Cognom' || soci_id,
email = 'soci' || soci_id || '@example.org';
UPDATE telefons_soci SET
numero = '600' || lpad((100000 + soci_id)::text, 6, '0');
-- Els imports i les dates es conserven: calen per provar de debò
-- Les taules d'auditoria contenen dades antigues: es buiden
TRUNCATE auditoria_socis;
COMMIT;Tres regles perquè una anonimització serveixi d'alguna cosa:
- S'executa com a part del procés de restauració, automàticament, mai a mà. Un pas manual s'oblidarà.
- Es conserva la forma de les dades —longituds, distribucions, volum— o l'entorn de proves deixarà d'assemblar-se a producció i els plans d'execució de 06-03 no seran comparables.
- Es comprova que no queda cap dada identificable en taules secundàries: auditoria, registres,
informes_esdeveniment, camps de text lliure on algú va anotar "trucar al 6XX del fill".
Aquell últim punt és el que més falla. Els camps d'observacions en text lliure són un pou de dades personals que cap anonimització per columnes no detecta.
Retenció
Una dada desada per sempre és un risc desat per sempre.
-- Exemple de política tècnica: historial de préstecs amb més de 5 anys
-- (el termini concret és una decisió jurídica, no tècnica)
UPDATE prestecs
SET soci_id = NULL
WHERE data_devolucio < CURRENT_DATE - INTERVAL '5 years'
AND soci_id IS NOT NULL;Fixa't que no s'esborra el préstec: es desvincula del soci. La biblioteca conserva l'estadística de circulació —quantes vegades es va prestar cada material, en quina sucursal, en quin mes—, que és el que necessita per a la seva gestió, i deixa de conservar qui va llegir què. És el tipus de solució que la tècnica sí que pot aportar a una decisió de retenció.
Això exigeix, és clar, que prestecs.soci_id admeti nuls i que les claus foranes ho permetin, cosa que és una decisió de disseny que convé prendre abans, al mòdul 4, i no quan arriba la política de retenció.
- Còpies de seguretat: lògiques i físiques
Passem a la segona pregunta de la regidoria. Comencem per l'afirmació que ordena tot el bloc:
Una còpia de seguretat no provada no és una còpia de seguretat: és una esperança.
PostgreSQL ofereix dues famílies de còpia, i no competeixen: es complementen.
Còpia lògica: pg_dump i pg_restore
Genera un fitxer amb les instruccions necessàries per reconstruir les dades: CREATE TABLE, COPY, CREATE INDEX.
# Format personalitzat (comprimit, restauració selectiva) — el recomanat
pg_dump -h localhost -U postgres -d biblioredb \
-F c -Z 6 -f /copies/biblioredb_2026-08-02.dump
# Només l'esquema, per a control de versions
pg_dump -d biblioredb --schema-only -f /copies/esquema_2026-08-02.sql
# Només unes taules
pg_dump -d biblioredb -t socis -t prestecs -F c -f /copies/parcial.dumpEn cas d'èxit, pg_dump no imprimeix res: només parla si hi ha problemes. Per veure el progrés en bases grans s'usa l'opció -v.
Restaurar:
# Base completa en una base nova
createdb -U postgres biblioredb_restaurada
pg_restore -U postgres -d biblioredb_restaurada -j 4 /copies/biblioredb_2026-08-02.dump
# Una sola taula, que és on brilla el format personalitzat
pg_restore -U postgres -d biblioredb -t socis /copies/biblioredb_2026-08-02.dump
# Veure què conté sense restaurar res
pg_restore -l /copies/biblioredb_2026-08-02.dump | head -20; ; Archive created at 2026-08-02 03:00:14 CEST ; dbname: biblioredb ; TOC Entries: 214 ; Compression: 6 ; Format: CUSTOM ; 215; 1259 16482 TABLE public socis postgres 216; 1259 16490 TABLE public prestecs postgres ...
Important: pg_dump no copia els rols ni les contrasenyes, que són globals del servidor. Calen a part:
Oblidar aquest fitxer és un clàssic: es restauren les dades i no es pot entrar perquè no existeix cap rol.
Còpia física: pg_basebackup
Copia els fitxers del directori de dades tal qual, a nivell de bloc.
Còpia lògica (pg_dump) |
Còpia física (pg_basebackup) |
|
|---|---|---|
| Què copia | Instruccions SQL per reconstruir | Els fitxers del clúster |
| Granularitat | Una taula, un esquema o tota la base | Tot el clúster, sense excepció |
| Portabilitat | Entre versions i arquitectures diferents | Només la mateixa versió major i arquitectura |
| Mida | Menor (sense índexs, comprimit) | Major (inclou índexs) |
| Velocitat de còpia | Lenta en bases grans | Ràpida |
| Velocitat de restauració | Lenta: reexecuta tot i reconstrueix índexs | Ràpida: és copiar fitxers |
| Permet recuperar a un instant concret? | No | Sí, amb arxivat del WAL |
| Ús típic | Migracions, còpies per taula, canvi de versió | Recuperació davant de desastre |
La resposta correcta per a BiblioRed és "les dues", i l'apartat 17 concreta el pla.
- Completa, diferencial i incremental
La classificació clàssica, aplicable a qualsevol sistema de còpies:
| Tipus | Què copia | Espai | Temps de còpia | Temps de restauració |
|---|---|---|---|---|
| Completa | Tot | Màxim | Màxim | Mínim: un sol fitxer |
| Diferencial | El que ha canviat des de l'última completa | Mitjà, creixent | Mitjà | Mitjà: completa + última diferencial |
| Incremental | El que ha canviat des de l'última còpia de qualsevol tipus | Mínim | Mínim | Màxim: completa + totes les incrementals |
El compromís és sempre el mateix: espai i temps de còpia enfront de temps i complexitat de restauració. I hi ha un factor que decideix més que els números:
Com més fitxers necessiti una restauració, més probable és que un falli. Una cadena incremental de trenta baules es trenca si falta la número disset.
A PostgreSQL, la traducció pràctica d'aquest esquema és:
- La completa és el
pg_basebackup. - El paper de les incrementals el compleix l'arxivat del WAL, que és continu en lloc de periòdic. I és una solució millor que les incrementals clàssiques, perquè permet restaurar no només al moment d'una còpia, sinó a qualsevol instant.
(PostgreSQL 17 va afegir a més còpies incrementals natives amb pg_basebackup --incremental, útils en bases molt grans; l'arxivat del WAL continua sent la base de l'esquema.)
- Arxivat del WAL i recuperació a un instant concret
Aquí reapareix el registre d'escriptura anticipada de la lliçó 06-01, i la seva segona vida és tan important com la primera.
Recordaràs el mecanisme: abans de modificar una pàgina de dades, PostgreSQL escriu al WAL una anotació que descriu el canvi. Aquell registre conté, per tant, la història completa de totes les modificacions des del moment en què es va fer la còpia base.
Arxivat del WAL. Si conservem una còpia base i tots els segments de WAL generats des d'aleshores, podem reconstruir la base de dades en qualsevol instant posterior a la còpia base, aplicant el registre fins al punt desitjat. És la recuperació a un instant concret (point-in-time recovery, PITR).
graph LR
A["03:00<br/>Copia base completa<br/>(pg_basebackup)"] --> B["03:00 → 11:47<br/>Segments WAL<br/>arxivats sense parar"]
B --> C["11:47:03<br/>DELETE FROM socis<br/>sense WHERE"]
C --> D["11:52<br/>Es detecta<br/>el problema"]
D --> E["Restauracio:<br/>copia base + WAL<br/>fins a les 11:46:59"]
Configuració
# postgresql.conf wal_level = replica archive_mode = on archive_command = 'test ! -f /arxiu_wal/%f && cp %p /arxiu_wal/%f' archive_timeout = 300
| Paràmetre | Què fa |
|---|---|
wal_level = replica |
Genera prou informació al WAL per a recuperació i rèpliques |
archive_mode = on |
Activa l'arxivat |
archive_command |
Ordre que copia cada segment a un lloc segur |
archive_timeout = 300 |
Força el tancament d'un segment cada 5 minuts encara que no estigui ple. Acota la pèrdua màxima |
Aquell archive_timeout mereix atenció perquè és el que fixa el pitjor cas: sense ell, un segment de 16 MB a mig omplir podria no arxivar-se en hores, i aquella feina es perdria. Amb 300 segons, la pèrdua màxima està acotada a cinc minuts.
I un avís operatiu important: si archive_command falla, PostgreSQL conserva els segments i el disc s'omple. Un archive_command que apunta a una destinació inaccessible és una manera segura d'aturar el servidor en unes hores. Vigila'l:
SELECT archived_count, last_archived_wal, last_archived_time,
failed_count, last_failed_time
FROM pg_stat_archiver; archived_count | last_archived_wal | last_archived_time | failed_count | last_failed_time
----------------+--------------------------+-----------------------------+--------------+------------------
88412 | 000000010000000000000023 | 2026-08-02 13:15:02.118+02 | 0 |failed_count = 0 i last_archived_time recent: l'arxivat funciona. Aquestes dues columnes haurien de ser al tauler de supervisió.
La restauració a un instant concret
Escenari real: a les 11:47 algú executa en producció, creient que és a proves:
A les 11:52 es detecta. Els passos:
# 1) Aturar el servidor. No hi ha cap pressa que justifiqui saltar-se això.
sudo systemctl stop postgresql
# 2) Apartar el directori de dades actual. NO l'esborris: pot contenir
# el que s'ha escrit entre les 11:47 i les 11:52, i caldrà per reconciliar.
sudo mv /var/lib/postgresql/17/main /var/lib/postgresql/17/main_incident
# 3) Restaurar la còpia base
sudo -u postgres mkdir -p /var/lib/postgresql/17/main
sudo -u postgres tar -xzf /copies/base_2026-08-02/base.tar.gz \
-C /var/lib/postgresql/17/main
# 4) Indicar fins on aplicar el registre
sudo -u postgres tee -a /var/lib/postgresql/17/main/postgresql.auto.conf <<'EOF'
restore_command = 'cp /arxiu_wal/%f %p'
recovery_target_time = '2026-08-02 11:46:59+02'
recovery_target_action = 'promote'
EOF
sudo -u postgres touch /var/lib/postgresql/17/main/recovery.signal
# 5) Arrencar: PostgreSQL aplicarà el WAL fins a l'instant indicat
sudo systemctl start postgresqlAl registre del servidor:
LOG: starting point-in-time recovery to 2026-08-02 11:46:59+02 LOG: restored log file "000000010000000000000021" from archive LOG: restored log file "000000010000000000000022" from archive LOG: recovery stopping before commit of transaction 90412, time 2026-08-02 11:47:03.882+02 LOG: redo done at 0/22F1A8C0 LOG: selected new timeline ID: 2 LOG: archive recovery complete LOG: database system is ready to accept connections
Fixa't en la línia decisiva: recovery stopping before commit of transaction 90412. Aquella transacció 90412 és el DELETE. La recuperació s'atura just abans de confirmar-la.
Els dotze mil socis són de tornada. S'han perdut només les operacions d'aquells cinc minuts entre les 11:47 i les 11:52, que es reconstrueixen a mà des del directori apartat al pas 2 i des dels comprovants en paper del taulell.
Altres destinacions de recuperació possibles, a més de recovery_target_time:
| Paràmetre | Atura la recuperació en |
|---|---|
recovery_target_time |
Un instant |
recovery_target_xid |
Una transacció concreta (útil si es coneix del registre) |
recovery_target_lsn |
Una posició exacta del WAL |
recovery_target_name |
Un punt marcat abans amb pg_create_restore_point('abans_migracio') |
Aquell últim és or pur abans d'una migració d'esquema:
Eines de gestió
Fer tot això a mà és viable, però en producció s'usen eines que gestionen retenció, verificació i catàleg de còpies: pgBackRest, Barman i WAL-G són les tres habituals. Totes implementen el mateix mecanisme de fons que acabem de veure. Conèixer el mecanisme és el que permet entendre l'eina, i no a l'inrevés.
- La regla 3-2-1 i la verificació de les còpies
La regla 3-2-1
3 còpies de les dades · en 2 tipus de suport diferents · amb 1 còpia fora de l'emplaçament.
Aplicada a BiblioRed:
| Element | Implementació |
|---|---|
| Còpia 1 | Les dades en producció, al servidor de l'ajuntament |
| Còpia 2 | Còpia diària al sistema d'emmagatzematge en xarxa de l'ajuntament |
| Còpia 3 | Còpia xifrada a l'emmagatzematge del proveïdor extern contractat |
| 2 suports | Disc local del servidor + emmagatzematge en xarxa / emmagatzematge extern |
| 1 fora | La còpia al proveïdor extern, en una altra ciutat |
Afegits moderns a la regla, que l'experiència amb segrest de dades ha fet imprescindibles:
- 1 còpia immutable: emmagatzematge que no admet modificació ni esborrat durant un termini. Sense això, un atacant amb accés al servidor xifra o esborra també les còpies, que és exactament el que passa als incidents de segrest de dades.
- 0 errors a la verificació: cap còpia no compta fins que s'ha restaurat amb èxit.
La verificació: una còpia no provada no és una còpia
Aquest és el punt on falla la majoria de les organitzacions. El procés de còpia es configura, es veu que genera fitxers, i ningú no restaura mai fins al dia del desastre, que és el pitjor moment per descobrir que el fitxer estava truncat, que faltaven els rols, o que el procés portava set mesos copiant una base de dades buida.
Un guió de verificació automàtica, executat setmanalment:
#!/bin/bash
# verificar_copia.sh — restaura l'última còpia i comprova que té sentit
set -euo pipefail
COPIA=$(ls -t /copies/biblioredb_*.dump | head -1)
BD_PROVA="verificacio_$(date +%Y%m%d)"
echo "Verificant: $COPIA"
createdb "$BD_PROVA"
pg_restore -d "$BD_PROVA" -j 4 "$COPIA"
# Comprovacions de contingut: no n'hi ha prou que restauri sense error
SOCIS=$(psql -tAc "SELECT count(*) FROM socis" "$BD_PROVA")
PRESTECS=$(psql -tAc "SELECT count(*) FROM prestecs" "$BD_PROVA")
ULTIM=$(psql -tAc "SELECT max(data_prestec) FROM prestecs" "$BD_PROVA")
echo "Socis: $SOCIS | Préstecs: $PRESTECS | Últim préstec: $ULTIM"
if [ "$SOCIS" -lt 10000 ] || [ "$PRESTECS" -lt 2000000 ]; then
echo "ERROR: la còpia no conté els volums esperats"
dropdb "$BD_PROVA"
exit 1
fi
# La data de l'últim préstec ha de ser d'ahir o d'avui
if [[ "$ULTIM" < $(date -d 'yesterday' +%Y-%m-%d) ]]; then
echo "ERROR: la còpia és antiga; el procés pot estar aturat"
dropdb "$BD_PROVA"
exit 1
fi
dropdb "$BD_PROVA"
echo "Verificació correcta"Verificant: /copies/biblioredb_2026-08-02.dump Socis: 12000 | Préstecs: 2841077 | Últim préstec: 2026-08-01 Verificació correcta
Les comprovacions de volum i de data són el cor del guió. Una còpia que restaura sense errors però conté una base de dades buida restaura perfectament, i no serveix per a res. L'error clàssic —copiar la base de dades equivocada, o una que es va deixar d'usar— només es detecta comptant files.
I un cop l'any, un simulacre complet: restaurar en un servidor diferent, arrencar l'aplicació contra la còpia restaurada, i cronometrar quant es triga des de la trucada fins al servei funcionant. Aquell cronòmetre és el RTO real, i gairebé sempre és el triple del que s'havia estimat.
- RPO i RTO aplicats a BiblioRed
Dues sigles que ordenen qualsevol conversa sobre continuïtat, perquè converteixen "volem estar segurs" en dos números que es poden dissenyar i pressupostar.
| Sigla | Nom | Pregunta que respon | Es mesura en |
|---|---|---|---|
| RPO | Recovery Point Objective | Quantes dades ens podem permetre perdre? | Temps de treball perdut |
| RTO | Recovery Time Objective | Quant de temps pot estar caigut el servei? | Temps d'indisponibilitat |
Aplicats a les operacions de BiblioRed:
| Operació | RPO tolerable | RTO tolerable | Per què |
|---|---|---|---|
| Préstecs i devolucions | 5 minuts | 2 hores | El taulell pot apuntar en paper una estona, però no pot perdre préstecs: són llibres que no se sabrà on són |
Cobrament de multes (pagaments) |
0 | 2 hores | Són diners públics. Un cobrament perdut és una reclamació |
| Alta de socis | 1 hora | 4 hores | Es pot refer amb el formulari en paper |
| Inscripcions a esdeveniments | 1 hora | 4 hores | Molest, recuperable |
| Estadístiques i informes | 24 hores | 3 dies | No afecta el servei |
De la taula surten directament les decisions tècniques:
| Requisit | Conseqüència tècnica |
|---|---|
| RPO de 5 minuts | Arxivat del WAL amb archive_timeout = 300. Una còpia diària tota sola donaria un RPO de 24 hores |
| RPO de 0 en pagaments | Rèplica síncrona (synchronous_commit = remote_apply), o assumir que el paper del datàfon és el respatller |
| RTO de 2 hores | Còpia física verificada i un procediment escrit i assajat. Restaurar 2,8 milions de files amb pg_restore pot trigar més de dues hores |
| RTO de 2 hores amb marge | Rèplica de reserva llesta per promoure (la replicació de 03-01), que redueix el RTO a minuts |
Fixa't en la lògica: primer es decideix quant es pot perdre i quant es pot esperar; després es tria la tecnologia. Fer-ho a l'inrevés —muntar la infraestructura i veure quin RPO surt— és com dissenyar l'esquema sense conèixer el domini.
- El pla de còpies de BiblioRed, comentat
Amb tot l'anterior, aquest és el pla concret:
| Quan | Què | On | Retenció | Cobreix |
|---|---|---|---|---|
| Continu | Arxivat del WAL (archive_timeout = 300) |
Emmagatzematge en xarxa + còpia externa | 35 dies | RPO de 5 min; PITR |
| Diari 03:00 | pg_dump -F c complet |
Emmagatzematge en xarxa | 14 dies | Restauració per taula; migracions |
| Diari 03:30 | pg_dumpall --globals-only |
Al costat de la còpia diària | 14 dies | Rols i contrasenyes |
| Setmanal, diumenge 02:00 | pg_basebackup complet |
Emmagatzematge en xarxa + còpia externa xifrada | 8 setmanes | Base per a PITR; RTO baix |
| Mensual, dia 1 | Còpia completa | Emmagatzematge extern immutable | 12 mesos | Segrest de dades; auditoria |
| Setmanal, dilluns 06:00 | Verificació automàtica (guió de l'apartat 15) | Servidor de proves | Registre 12 mesos | Que les còpies serveixin |
| Anual | Simulacre complet de restauració | Servidor alternatiu | Informe | Mesurar el RTO real |
I les decisions que ho justifiquen, una per una:
Per què còpia lògica i física. La física dóna el RTO baix (restaurar és copiar fitxers) i és l'única base possible per al PITR. La lògica permet restaurar una sola taula —que és el que cal el 90 % de les vegades, perquè el desastre típic no és un disc mort, és un DELETE mal escrit— i és l'única que serveix per migrar a una versió major diferent.
Per què el WAL es reté 35 dies i la base setmanal 8 setmanes. El WAL només serveix si existeix la còpia base corresponent. Retenir 35 dies de WAL sense bases de més de 35 dies d'antiguitat seria inútil; a l'inrevés, retenir bases sense el seu WAL impedeix el PITR. La retenció del WAL ha de cobrir amb marge la de la còpia base més antiga que es vulgui usar per a PITR.
Per què una còpia immutable. Perquè un atacant que compromet el servidor i té les credencials de còpia esborrarà o xifrarà també les còpies. La còpia mensual immutable és l'últim recurs, i el seu RPO d'un mes és dolentíssim però infinitament millor que res.
Per què la verificació és setmanal i no mensual. Perquè la fallada típica del procés de còpia és silenciosa —un permís que va canviar, un disc ple, una ruta que va deixar d'existir— i una setmana és el temps màxim acceptable per detectar-la.
Per què el simulacre anual, si ja hi ha verificació setmanal. Perquè són coses diferents. La verificació comprova que el fitxer de còpia és bo; el simulacre comprova que el procediment funciona amb persones pel mig: que algú sap on és el document, que les credencials de l'emmagatzematge extern continuen sent vàlides, que la persona que el va escriure no és l'única que sap fer-ho, i quant es triga de debò.
Què falta en aquest pla. Convé dir-ho també: no hi ha rèplica de reserva. Amb el RTO de 2 hores de l'apartat 16 no és imprescindible, però és la primera inversió que caldria si la biblioteca decidís baixar aquell objectiu. La replicació primari-secundaris de 03-01 és exactament aquella peça.
- Els primers minuts després d'un esborrat accidental
El moment en què més mal es fa és el que segueix immediatament a l'error, quan algú intenta arreglar-lo de pressa. Aquest és el procediment, i convé tenir-lo imprès.
Minut 0 — Parar. No executis res més. No intentis "tornar a inserir les dades". Cada escriptura posterior complica la reconciliació i pot sobreescriure informació recuperable.
Minut 1 — Encara està oberta la transacció? Si el DELETE es va executar dins d'un BEGIN sense confirmar, la solució és un ROLLBACK i prou (06-01). Comprovar qui té què obert:
SELECT pid, usename, state, now() - xact_start AS duracio, left(query, 60) AS consulta
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'active')
ORDER BY xact_start;pid | usename | state | duracio | consulta -------+----------+---------------------+-----------------+------------------------------------ 41902 | u_alsina | idle in transaction | 00:03:12.881021 | DELETE FROM socis
Aquí està: la transacció continua oberta. Un ROLLBACK en aquella sessió resol l'incident sencer. Val la pena comprovar-ho abans que res.
Minut 2 — Aïllar. Si ja està confirmat, impedir noves escriptures mentre es decideix:
-- Tallar l'accés de l'aplicació sense aturar el servidor
REVOKE CONNECT ON DATABASE biblioredb FROM bibliored_app, bibliored_taulell;
-- Tancar les sessions existents d'aquells rols
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'biblioredb'
AND usename IN ('app_bibliored','u_alsina','u_pereda');Minut 3 — Determinar l'abast exacte. Quina taula, quantes files, a quina hora, amb quin usuari. La taula d'auditoria de l'apartat 10 respon tot això:
SELECT operacio, min(moment) AS des_de, max(moment) AS fins_a,
usuari_bd, count(*) AS files
FROM auditoria_socis
WHERE moment > now() - INTERVAL '1 hour'
GROUP BY operacio, usuari_bd;operacio | des_de | fins_a | usuari_bd | files ----------+-----------------------------+-----------------------------+------------+------- DELETE | 2026-08-02 11:47:02.118+02 | 2026-08-02 11:47:04.882+02 | u_alsina | 12000
Minut 5 — Decidir la via de recuperació:
| Situació | Via |
|---|---|
| Transacció encara oberta | ROLLBACK |
Poques files i hi ha auditoria amb dades_abans |
Reinserir des de la taula d'auditoria |
| Una taula completa, i la còpia diària val | pg_restore -t socis en una base auxiliar, i copiar les files |
| Abast ampli, o calen dades posteriors a la còpia | PITR a l'instant anterior (apartat 14) |
L'opció de la taula d'auditoria, quan aplica, és la més quirúrgica:
INSERT INTO socis (soci_id, nom, cognoms, email, data_alta, sucursal_id, actiu)
SELECT (dades_abans ->> 'soci_id')::int,
dades_abans ->> 'nom',
dades_abans ->> 'cognoms',
dades_abans ->> 'email',
(dades_abans ->> 'data_alta')::date,
(dades_abans ->> 'sucursal_id')::int,
(dades_abans ->> 'actiu')::boolean
FROM auditoria_socis
WHERE operacio = 'DELETE'
AND moment BETWEEN '2026-08-02 11:47:00+02' AND '2026-08-02 11:47:10+02';Minut 30 — Després. Restablir els permisos, comprovar la coherència amb les consultes de control de 05-03, i escriure què ha passat. L'informe posterior no busca culpables: busca per què va ser possible. Gairebé sempre la resposta és alguna d'aquestes tres: la connexió tenia més permisos dels necessaris, no hi havia manera de distingir l'entorn de producció del de proves, o no existia procediment per a operacions massives.
Tres mesures preventives que surten d'aquí:
-- 1) Que el taulell no pugui esborrar res
REVOKE DELETE ON socis, prestecs, multes, pagaments FROM bibliored_taulell;
-- 2) Que l'indicador de psql cridi en producció
-- (a ~/.psqlrc del servidor de producció)
\set PROMPT1 '%[%033[1;31m%]PRODUCCIÓ%[%033[0m%] %/=# '
-- 3) Punt de restauració abans de qualsevol operació massiva
SELECT pg_create_restore_point('abans_de_purga_historic');
- L'equivalent mínim a SQLite
Per tancar, la versió reduïda del mateix problema.
SQLite no té usuaris, ni rols, ni GRANT: els permisos són els del fitxer al sistema operatiu. Qui pot llegir el fitxer, ho llegeix tot; qui el pot escriure, ho escriu tot. Tota la primera meitat d'aquesta lliçó no té equivalent, i aquesta és una raó important per no usar SQLite en un sistema multiusuari amb dades personals.
Les còpies sí que tenen equivalent, i és senzill:
# CORRECTE: còpia en calent i coherent, amb la base en ús
sqlite3 biblioredb.db ".backup '/copies/biblioredb_2026-08-02.db'"
# CORRECTE: bolcat lògic, equivalent a pg_dump
sqlite3 biblioredb.db ".dump" > /copies/biblioredb_2026-08-02.sql
# CORRECTE: comprovar la integritat del fitxer
sqlite3 biblioredb.db "PRAGMA integrity_check;"I l'advertiment central:
Copiar el fitxer
.dbambcpmentre la base està en ús produeix una còpia corrupta, perquè pot capturar el fitxer a mitja transacció, i en mode WAL deixa fora el fitxer-walamb els canvis recents. Copiar el fitxer directament només és segur amb la base tancada i sense cap connexió oberta.
.backup sí que és segur en calent: usa el mecanisme intern de còpia de SQLite, que garanteix una imatge coherent. És una diferència d'una línia que separa una còpia vàlida d'una inservible.
Errors Habituals i Consells
Connectar l'aplicació com a superusuari. Anul·la tota la configuració de permisos i converteix qualsevol injecció en un compromís del servidor. És l'error més greu i el més fàcil de corregir.
Donar SELECT sobre les taules i oblidar USAGE sobre l'esquema. El rol no veurà res i rebràs un informe d'error confús. És la fallada número u en configurar permisos per primera vegada.
Usar GRANT ... ON ALL TABLES i creure que cobreix el futur. Només afecta les taules existents. Sense ALTER DEFAULT PRIVILEGES, la taula que es creï demà quedarà inaccessible o —pitjor, segons com estigui muntat— accessible a qui no ha de ser.
Deixar trust a pg_hba.conf. Encara que sigui "un moment, per provar". Aquell moment dura tres anys.
Usar sslmode=require i creure que és segur. Xifra, però no verifica amb qui parla. Usa verify-full.
Escapar cometes a mà en lloc de parametritzar. N'hi ha prou amb un lloc oblidat de dos-cents. Les consultes parametritzades no tenen aquell mode de fallada.
Mostrar l'error de la base de dades a l'usuari final. Regala l'esquema. Registra l'error complet internament i mostra un missatge genèric.
Copiar la base de producció al portàtil de desenvolupament sense anonimitzar. És la fuita de dades més comuna i la que menys es percep com a tal. Anonimitza com a part automàtica del procés de restauració.
Activar log_statement = 'all' en una base amb dades personals. Els correus i telèfons acaben en fitxers de registre amb menys protecció i més còpies que la base de dades.
Tenir còpies i no haver restaurat mai. És l'error d'aquesta lliçó. Una còpia no provada no és una còpia. Verifica setmanalment de manera automàtica i fes un simulacre complet a l'any.
No copiar els rols. pg_dump no els inclou. Sense pg_dumpall --globals-only, restauraràs les dades i no podràs entrar.
No vigilar pg_stat_archiver. Si archive_command falla, el WAL s'acumula, el disc s'omple i el servidor s'atura. failed_count ha d'estar en un tauler.
Confondre emmascarar amb anonimitzar. m***@example.org continua sent una dada personal en un registre que conté el nom i la sucursal. Anonimitzar és substituir, no tapar.
Consell final: escriu el procediment de recuperació i guarda'l fora del sistema que protegeix. Un document de restauració allotjat únicament al servidor que s'ha incendiat és un acudit que només fa gràcia abans de l'incendi. En paper, a l'emmagatzematge extern, i amb les credencials necessàries custodiades per almenys dues persones.
Exercicis
Exercici 1: Un quart rol amb mínim privilegi
La regidoria de Cultura de Vallmar contracta una empresa externa per analitzar l'ús de les biblioteques durant sis mesos. L'empresa necessita: llegir els préstecs, exemplars, materials, sucursals i esdeveniments; no ha de poder identificar cap soci; no ha de poder escriure res; i el seu accés ha de caducar automàticament el 31 de desembre de 2026.
Escriu el SQL complet: el rol, els permisos, qualsevol vista que necessitis, i la comprovació que no pot arribar a les dades personals.
Exercici 2: Diagnosticar i corregir una configuració
Un tècnic ha deixat aquesta configuració al servidor de producció de BiblioRed. Identifica tots els problemes de seguretat, ordenats per gravetat, i escriu la correcció de cadascun.
CREATE ROLE app_bibliored LOGIN PASSWORD 'bibliored2026' SUPERUSER;
GRANT ALL ON ALL TABLES IN SCHEMA public TO PUBLIC;Exercici 3: Dissenyar el pla de còpies d'un cas nou
La xarxa de biblioteques de Vallmar afegeix un servei de préstec d'instruments musicals, amb la seva pròpia base de dades instruments. Les seves característiques: 400 instruments, 900 usuaris, unes 30 operacions al dia, i els imports de les fiances (fins a 300 € per instrument) es registren en una taula fiances.
Defineix el RPO i el RTO de cada tipus d'operació, justifica'ls, i escriu el pla de còpies que se'n deriva. Explica en què es diferencia del pla de BiblioRed de l'apartat 17 i per què.
Solucions
Solució 1
-- =========================================================
-- 1) Vista sense cap dada identificable de socis
-- =========================================================
CREATE VIEW v_prestecs_analitica AS
SELECT p.prestec_id,
-- Pseudònim estable però no invertible sense la sal
encode(digest(p.soci_id::text || current_setting('app.sal_analitica'), 'sha256'), 'hex')
AS soci_pseudonim,
p.data_prestec,
p.data_devolucio_prevista,
p.data_devolucio,
e.sucursal_id,
e.material_id,
m.tipus AS tipus_material
FROM prestecs p
JOIN exemplars e ON e.exemplar_id = p.exemplar_id
JOIN materials m ON m.material_id = e.material_id;
-- =========================================================
-- 2) Rol de grup amb permisos mínims
-- =========================================================
CREATE ROLE bibliored_analitica NOLOGIN;
GRANT CONNECT ON DATABASE biblioredb TO bibliored_analitica;
GRANT USAGE ON SCHEMA public TO bibliored_analitica;
GRANT SELECT ON v_prestecs_analitica,
exemplars, materials, sucursals,
v_esdeveniments_publicats, tipus_esdeveniment, sales
TO bibliored_analitica;
-- Explícit i verificable: res del que és sensible
REVOKE ALL ON socis, telefons_soci, multes, pagaments,
inscripcions, auditoria_socis, prestecs
FROM bibliored_analitica;
-- Que un objecte futur no se li concedeixi per descuit
ALTER DEFAULT PRIVILEGES IN SCHEMA public
REVOKE ALL ON TABLES FROM bibliored_analitica;
-- =========================================================
-- 3) Rol de connexió amb caducitat i límit
-- =========================================================
CREATE ROLE u_consultora LOGIN
PASSWORD 'contrasenya-llarga-generada-aleatoriament'
VALID UNTIL '2026-12-31 23:59:59+01'
CONNECTION LIMIT 3;
GRANT bibliored_analitica TO u_consultora;I a pg_hba.conf, restringint l'origen i obligant a TLS:
Comprovació:
SET ROLE bibliored_analitica;
SELECT count(*) FROM v_prestecs_analitica; -- ha de funcionar
SELECT email FROM socis LIMIT 1; -- ha de fallar
SELECT * FROM prestecs LIMIT 1; -- ha de fallar (soci_id en clar)
INSERT INTO prestecs (soci_id) VALUES (14); -- ha de fallar
RESET ROLE;count --------- 2841077 ERROR: permission denied for table socis ERROR: permission denied for table prestecs ERROR: permission denied for table prestecs
Punts clau de la solució:
- Es revoca
prestecsi es dóna accés només a la vista. DonarSELECTsobreprestecshauria lliuratsoci_iden clar, que combinat amb les dates permet reidentificar persones concretes amb poc esforç. VALID UNTILfa que l'accés caduqui sol. Confiar que algú se'n recordi de revocar-lo al desembre és confiar massa.CONNECTION LIMIT 3evita que una eina d'anàlisi mal configurada obri dues-centes connexions i afecti el servei del taulell.hostsslamb l'adreça concreta limita l'accés a la xarxa de la consultora i obliga al xifratge.- Nota de compliance: el pseudònim fa més difícil la reidentificació, però un conjunt amb dates, sucursal i materials pot continuar sent reidentificable per combinació. Que aquest tractament sigui suficient i amb quina base jurídica se cedeix a un tercer és una qüestió que ha de validar el delegat de protecció de dades de l'ajuntament, no l'equip tècnic.
Solució 2
Problema 1 (crític) — SUPERUSER a la connexió de l'aplicació.
Anul·la tots els permisos i totes les polítiques de fila, i converteix qualsevol injecció en execució d'ordres del sistema operatiu mitjançant COPY ... FROM PROGRAM.
ALTER ROLE app_bibliored NOSUPERUSER;
ALTER ROLE app_bibliored CONNECTION LIMIT 40;
GRANT bibliored_app TO app_bibliored;Problema 2 (crític) — GRANT ALL ... TO PUBLIC.
PUBLIC inclou tots els rols presents i futurs. Qualsevol que aconsegueixi connectar-se té control total sobre totes les taules, inclòs DELETE i TRUNCATE.
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON DATABASE biblioredb FROM PUBLIC;
-- I aplicar l'script de rols de l'apartat 4Problema 3 (crític) — Injecció SQL per concatenació.
A més, s'elimina el SELECT *: la consulta ha de demanar només les columnes que usa, cosa que evita exposar email per accident i facilita l'Index Only Scan de 06-03.
Problema 4 (crític) — local all all trust.
Qualsevol usuari del sistema operatiu del servidor entra com a qualsevol rol, inclòs postgres, sense contrasenya.
Problema 5 (greu) — host all all 0.0.0.0/0.
La base de dades accepta connexions des de qualsevol lloc d'internet, a qualsevol base i amb qualsevol usuari.
hostssl biblioredb bibliored_taulell 10.20.0.0/16 scram-sha-256 hostssl biblioredb app_bibliored 10.20.5.11/32 scram-sha-256 host all all 0.0.0.0/0 reject
Problema 6 (greu) — md5 en lloc de scram-sha-256.
Mètode amb debilitats conegudes. Es canvia el mètode i es reassignen les contrasenyes, perquè el canvi de mètode no reconverteix sol les existents:
SET password_encryption = 'scram-sha-256';
ALTER ROLE app_bibliored PASSWORD 'nova-contrasenya-llarga';Problema 7 (greu) — Sense TLS.
Totes les línies eren host, no hostssl: el trànsit viatja en clar. Corregit al problema 5, més ssl = on a postgresql.conf.
Problema 8 (moderat) — Contrasenya feble i previsible.
bibliored2026 s'endevina al primer intent d'un atac dirigit. Contrasenya llarga generada aleatòriament, guardada en un gestor de secrets, mai al repositori de codi, i amb rotació planificada.
Ordre d'intervenció recomanat: primer tancar pg_hba.conf (problemes 4, 5, 6 i 7), que atura l'accés indegut de seguida; després treure SUPERUSER i PUBLIC (1 i 2), que limiten el dany; i en paral·lel corregir el codi (3), que requereix desplegament. I tan bon punt es tanqui l'urgent: revisar els registres per determinar si aquesta configuració va ser aprofitada, cosa que és un incident que s'ha de comunicar pels canals de l'organització.
Solució 3
RPO i RTO per operació:
| Operació | RPO | RTO | Justificació |
|---|---|---|---|
Registre de fiances (fiances) |
0 | 4 hores | Són diners de ciutadans, fins a 300 € per instrument. Una fiança perduda és una reclamació i un problema d'intervenció municipal |
| Préstec i devolució d'instruments | 1 hora | 8 hores | 30 operacions al dia: en una hora es perden 2 o 3 operacions, reconstruïbles amb el resguard en paper |
| Alta d'usuaris | 24 hores | 24 hores | Es refà amb el formulari d'alta |
| Catàleg d'instruments | 24 hores | 24 hores | Canvia molt poc |
Pla de còpies derivat:
| Quan | Què | On | Retenció |
|---|---|---|---|
| Continu | Arxivat del WAL, archive_timeout = 600 |
Emmagatzematge en xarxa + extern | 35 dies |
| Diari 03:00 | pg_dump -F c complet |
Emmagatzematge en xarxa | 30 dies |
| Diari 03:15 | pg_dumpall --globals-only |
Al costat de la còpia diària | 30 dies |
| Setmanal | pg_basebackup |
Emmagatzematge en xarxa + extern xifrat | 8 setmanes |
| Mensual | Còpia completa | Emmagatzematge extern immutable | 12 mesos |
| Setmanal | Verificació automàtica amb restauració | Servidor de proves | Registre 12 mesos |
| Anual | Simulacre complet | Servidor alternatiu | Informe |
Diferències amb el pla de BiblioRed i per què:
-
archive_timeout = 600en lloc de 300. Amb 30 operacions al dia, un segment de WAL cada 10 minuts és més que suficient i genera molts menys fitxers per arxivar i retenir. El RPO efectiu de 10 minuts continua sent millor que el requisit d'1 hora del préstec. -
Retenció de la còpia lògica de 30 dies en lloc de 14. La base és diminuta —900 usuaris i 400 instruments—, així que 30 còpies diàries comprimides ocupen uns pocs megabytes. Quan l'espai no és una restricció, es reté més, perquè l'error que més triga a detectar-se és la corrupció lògica silenciosa: algú va modificar malament unes dades fa tres setmanes i ningú no ho va veure.
-
RTO més folgat (4-8 hores enfront de 2). El servei d'instruments no és crític per al funcionament diari de les biblioteques i admet operar en paper durant una jornada. Això fa innecessària qualsevol inversió en rèplica de reserva.
-
El RPO de 0 en
fiancesno obliga aquí a rèplica síncrona. A diferència dels pagaments de multes de BiblioRed —que arriben per datàfon i no deixen comprovant a la biblioteca—, la fiança d'un instrument es cobra amb un resguard en paper signat per l'usuari. Aquell resguard és el respatller del RPO 0, i és molt més barat que la infraestructura equivalent. És un bon exemple que un requisit de continuïtat no sempre es resol amb tecnologia. -
El mateix règim de verificació i simulacre. Aquest punt no es relaxa per la mida de la base. La probabilitat que un procés de còpies falli silenciosament no depèn de quantes files hi hagi, i una base petita sol tenir menys supervisió, no més. És exactament on més falta fa la verificació automàtica.
Conclusió
Les dues preguntes de la regidoria de Vallmar ja tenen resposta, i totes dues són respostes concretes.
Qui pot consultar els telèfons i els correus dels socis? El rol bibliored_app, perquè l'aplicació web necessita enviar els avisos de venciment, i ningú més. El personal de taulell pot modificar un correu però no llegir-lo en un llistat. La direcció accedeix a v_socis_public, on aquelles columnes no existeixen. La consultora externa treballa sobre v_prestecs_analitica amb pseudònims amb sal. I tot canvi sobre socis queda registrat a auditoria_socis amb el rol, l'adreça d'origen i el valor anterior. La resposta ja no és "no ho sé": és un script de GRANT que es pot llegir, comprovar amb has_column_privilege i auditar.
Què passaria si el disc morís aquesta nit? Es restauraria l'últim pg_basebackup, s'aplicaria el WAL arxivat fins a l'últim segment disponible, i el servei tornaria amb una pèrdua màxima de cinc minuts i en menys de dues hores. I aquella afirmació no és una esperança: és un procediment escrit, verificat automàticament cada dilluns amb un guió que compta files i comprova dates, i assajat sencer una vegada l'any amb cronòmetre.
Pel camí hem recorregut les capes completes. L'autenticació, amb rols que són alhora usuaris i grups, scram-sha-256 com a mètode i aquell pg_hba.conf que es llegeix de dalt a baix i ha d'acabar en reject. L'autorització, amb la seva jerarquia de base, esquema i taula —i el USAGE que tothom oblida—, els privilegis a nivell de columna, ALTER DEFAULT PRIVILEGES per als objectes que encara no existeixen, i el principi de mínim privilegi convertit en tres rols concrets i comprovables. Les vistes per amagar columnes i la seguretat a nivell de fila per amagar files, amb l'advertiment que ni l'una ni l'altra no protegeixen d'un superusuari. La injecció SQL, amb el cercador del catàleg tornant dotze mil correus i la consulta parametritzada convertint el mateix atac en una cerca sense resultats, perquè no escapa res: separa els camins. El xifratge en trànsit amb verify-full i no amb require, en repòs a nivell de disc, i les contrasenyes que no es xifren sinó que es deriven amb sal i amb cost. L'auditoria amb SECURITY DEFINER, que permet escriure al registre sense poder-lo esborrar. I les dades personals, amb la minimització que és l'única mesura que no pot fallar, la pseudonimització que sense sal no és pseudonimització, l'anonimització que ha de ser un pas automàtic del procés de restauració, i el recordatori —que convé repetir— que aquests són mecanismes tècnics i que el compliment normatiu el decideix un professional de compliance, no l'equip de desenvolupament.
I el bloc de còpies, on el WAL de 06-01 ha tingut la seva segona vida: el que en aquella lliçó era el mecanisme que garantia la durabilitat d'un COMMIT davant d'un tall de llum, aquí s'ha convertit en la història completa de les modificacions que permet aturar la recuperació just abans de la transacció 90412, aquella que va esborrar dotze mil socis a les 11:47:03. Hem vist la còpia lògica i la física amb els seus papers diferents i complementaris, la regla 3-2-1 amb els seus afegits moderns d'immutabilitat, el RPO i el RTO com els dos números que converteixen "volem estar segurs" en decisions d'enginyeria, un pla de còpies comentat decisió per decisió, i el procediment dels primers minuts, que comença per allò més contraintuïtiu i més eficaç: parar i comprovar si la transacció continua oberta.
Amb això es tanca el mòdul 6 i, amb ell, el bloc teòric del curs. Val la pena mirar enrere el recorregut complet. Vam començar al mòdul 1 amb un full de càlcul desbordat i vam obrir el SGBD per veure'n les peces per dins. Al 2 vam aprendre el model relacional i SQL fins als agregats i la integritat referencial. Al 3 vam sortir del món relacional per entendre NoSQL, el teorema CAP i la consistència eventual, i vam tornar sabent què es guanya i què es paga a cada costat. Al 4 vam dissenyar l'esquema de BiblioRed des del diagrama entitat-relació fins a l'últim CHECK. Al 5 el vam sotmetre a un examen formal, vam trobar una fallada real a multes i vam justificar per escrit cada desnormalització. I al 6 hem deixat de mirar l'esquema per mirar el sistema en funcionament: les transaccions que passen senceres o no passen, la concurrència que trenca el codi correcte tan bon punt hi ha dues persones, els índexs que van convertir catorze segons en quaranta mil·lisegons, i la seguretat i les còpies que separen una base de dades d'un accident esperant a passar.
El que queda del curs ja no és teoria nova: és fer-ho amb les mans. El mòdul 7, Exercicis Pràctics, és on tot l'anterior es converteix en destresa, i està organitzat seguint el mateix recorregut: exercicis de SQL sobre l'esquema de BiblioRed que ja coneixes pam a pam (07-01); exercicis de disseny d'esquemes, amb dominis nous que hauràs de modelar des de zero (07-02); exercicis de normalització, amb taules que amaguen dependències funcionals que ara saps detectar (07-03); i exercicis de consultes avançades i transaccions, on tornaran les dues sessions psql en paral·lel, els nivells d'aïllament i els plans d'execució d'aquest mòdul (07-04). Després, el mòdul 8 recorrerà tres casos d'estudi complets —relacional, no relacional i de persistència poliglota— i el mòdul 9 reunirà els llibres, cursos i eines amb què continuar pel teu compte. Obre els dos terminals, tingues l'esquema de BiblioRed a mà, i ens veiem al primer exercici.
Fonaments de Bases de Dades
Mòdul 1: Introducció a les Bases de Dades
- Conceptes Bàsics de Bases de Dades
- Tipus de Bases de Dades
- Història i Evolució de les Bases de Dades
- Sistemes Gestors de Bases de Dades i Arquitectura
Mòdul 2: Bases de Dades Relacionals
- Model Relacional
- Llenguatge SQL
- Operacions Bàsiques en SQL
- Consultes Multitaula: JOIN i Subconsultes
- Agregació i Agrupació de Dades
- Integritat Referencial
Mòdul 3: Bases de Dades No Relacionals
- Introducció a NoSQL
- Tipus de Bases de Dades NoSQL
- Modelatge de Dades en NoSQL
- Comparació entre Bases de Dades Relacionals i No Relacionals
Mòdul 4: Disseny d'Esquemes
- Principis de Disseny d'Esquemes
- Diagrames Entitat-Relació (ER)
- Transformació de Diagrames ER a Esquemes Relacionals
- Tipus de Dades i Restriccions
Mòdul 5: Normalització
Mòdul 6: Transaccions, Rendiment i Seguretat
- Transaccions i Propietats ACID
- Concurrència i Nivells d'Aïllament
- Índexs i Optimització de Consultes
- Seguretat, Permisos i Còpies de Seguretat
Mòdul 7: Exercicis Pràctics
- Exercicis de SQL
- Exercicis de Disseny d'Esquemes
- Exercicis de Normalització
- Exercicis de Consultes Avançades i Transaccions
Mòdul 8: Casos d'Estudi
- Cas d'Estudi: Base de Dades Relacional
- Cas d'Estudi: Base de Dades No Relacional
- Cas d'Estudi: Persistència Poliglota
