La lliçó anterior va deixar gestor-tasques amb els fitxers correctes a dins i ben tractats. Queda l'assumpte que vam anunciar en tancar el mòdul 7 i que hem anat ajornant deliberadament: hi ha un config.js amb la contrasenya de la base de dades a l'historial des de fa vuit mesos, i el repositori fa quatre mesos que és públic perquè el Diego hi pugui col·laborar des del seu fork.

Aquesta lliçó tanca tres promeses del curs: la de la lliçó 04-03, on vam veure credential.helper store i vam advertir que desa les credencials en text pla remetent aquí; la de la lliçó 08-03, on vam dir que git rm --cached no esborra un secret de l'historial; i la de la lliçó 06-01, on els hooks van quedar com a eina de comoditat, no de seguretat.

L'enfocament és preventiu i de remediació. S'organitza en quatre blocs: per què un repositori és el pitjor lloc possible per a un secret, com detectar una fuita abans i després que passi, què cal fer exactament si ja ha passat (i l'ordre dels passos importa moltíssim), i com mantenir la confiança en l'autenticació i en l'autoria.

Avís previ. Aquest material és formatiu i general. En entorns regulats —dades personals, sanitàries, financeres, sector públic— una filtració de credencials pot comportar obligacions legals de notificació amb terminis estrictes. Abans d'actuar, avisa el teu responsable de seguretat o de compliment normatiu. Reescriure un historial també pot tenir implicacions d'auditoria. El que segueix són bones pràctiques tècniques, no assessorament legal.

Contingut

  1. Per què un repositori és un lloc pèssim per a un secret
  2. Què compta com a secret
  3. Gestió correcta: variables d'entorn i gestors de secrets
  4. Detecció: hooks de client i anàlisi al CI
  5. Si ja s'ha filtrat: el procediment per ordre
  6. Pas 1 — Rotar la credencial
  7. Pas 2 — Reescriure l'historial amb git filter-repo
  8. Pas 3 — Coordinar-se amb l'equip
  9. Pas 4 — Les còpies que queden a les plataformes
  10. Per què git rm no serveix
  11. Autenticació: claus SSH, ssh-agent i gestors de credencials
  12. Signar commits i etiquetes
  13. Higiene: permisos, branques protegides i --force
  14. Abans de fer públic un repositori

  1. Per què un repositori és un lloc pèssim per a un secret

Un repositori de Git té quatre propietats que, combinades, el converteixen en el pitjor contenidor imaginable per a una credencial:

1. Es clona sencer. Recorda de la lliçó 01-01 que Git és distribuït: git clone no descarrega la punta, descarrega tot l'historial. Cada persona que hagi clonat alguna vegada té una còpia completa del teu secret al seu disc.

2. És immutable per disseny. A la lliçó 01-04 vam veure que un commit és immutable: el seu SHA és el hash del seu contingut. Un blob amb la contrasenya continua existint mentre alguna cosa el referenciï, i esborrar-lo requereix reescriure tots els commits posteriors, és a dir, trencar la regla d'or de la lliçó 05-01.

3. Es replica sense control. Forks (el Diego en té un), rèpliques, còpies de seguretat, memòries cau del CI, imatges de contenidor que van fer git clone, entorns de desenvolupament de gent que ja no és a l'empresa.

4. És molt fàcil de cercar. No cal llegir commit a commit. Una sola ordre ho troba tot:

git log --all -p -S 'contrasenya' --oneline
git rev-list --all | xargs git grep -n 'clau' 2>/dev/null

En un repositori públic, hi ha eines i serveis que escanegen contínuament els repositoris acabats de publicar buscant patrons de credencials. El temps mitjà entre publicar una clau vàlida i el primer intent d'ús es mesura en minuts, no en dies.

flowchart TD
    A["Commit amb el secret"] --> B["push a git.exemple.cat"]
    B --> C["Clon de l'Ana"]
    B --> D["Clon del Bruno"]
    B --> E["Clon de la Carla"]
    B --> F["Fork del Diego"]
    B --> G["Memòria cau del CI"]
    B --> H["Còpia de seguretat"]
    F --> I["Clons del fork"]
    B --> J["Escàners automàtics<br/>si és públic"]

Cada node d'aquest graf és una còpia sobre la qual no tens cap control. D'aquí en surt la conclusió que governa tota la lliçó: l'única acció que de debò neutralitza un secret filtrat és invalidar-lo. Tota la resta —reescriure, esborrar, forçar— és neteja posterior, important però secundària.

  1. Què compta com a secret

La llista és més llarga del que la gent suposa. Tot el que ve a continuació mai no ha de ser en un repositori:

Categoria Exemples
Contrasenyes De bases de dades, de serveis interns, de comptes de servei, de correu
Claus d'API i tokens De qualsevol proveïdor, tokens d'accés personal (PAT), tokens de sessió, claus de webhook
Claus criptogràfiques Claus privades SSH (id_rsa, id_ed25519), certificats amb clau privada (.pem, .p12, .pfx), claus de signatura
Cadenes de connexió postgres://usuari:clau@host/bd, mongodb+srv://..., qualsevol URL amb credencials incrustades
Secrets d'aplicació Claus de signatura de sessions i de JWT, claus de xifratge en repòs, llavors de generació
Credencials d'infraestructura Claus d'accés a proveïdors de núvol, fitxers de credencials de desplegament, tokens de registre de contenidors
Dades personals Bolcats de base de dades amb dades reals d'usuaris, fitxers d'exportació, captures amb dades identificables
Configuració sensible URL de serveis interns no publicats, rutes de xarxa, adreces d'infraestructura privada, noms de host interns

Dues categories que la gent oblida sistemàticament:

  • Les dades personals als bolcats de prova. Un dump.sql "de proves" amb correus i telèfons reals d'usuaris és una filtració de dades personals, amb les conseqüències legals que això comporta.
  • La informació d'infraestructura. No és una credencial, però un atacant que sap com es diuen els teus servidors interns i quins ports fan servir té mitja feina feta.

El criteri pràctic:

Si un valor caduca, es rota o es pot revocar, és un secret. Si en veure'l un desconegut podria fer alguna cosa que tu no voldries, és un secret. En cas de dubte, és un secret.

  1. Gestió correcta: variables d'entorn i gestors de secrets

La regla estructural és simple:

La configuració que canvia entre entorns va fora del codi. Els secrets van fora del repositori, sempre.

Nivell 1: variables d'entorn i .env ignorat

És el que vam veure al final de la lliçó 08-03, i per a un projecte petit ja n'hi ha prou.

// app.js — el codi llegeix de l'entorn, mai no conté el valor
const config = {
  baseDadesUrl: process.env.BASE_DADES_URL,
  clauCorreu:  process.env.API_CORREU_CLAU,
  sessioSecret: process.env.SESSIO_SECRET,
  port: process.env.PORT || 3000,
};

// Fallar aviat i amb claredat si falta alguna cosa
for (const [clau, valor] of Object.entries(config)) {
  if (valor === undefined) {
    throw new Error(`Falta la variable d'entorn per a "${clau}". Copia .env.exemple a .env.`);
  }
}

Amb el .env real ignorat i el .env.exemple versionat amb valors evidentment falsos:

.env
.env.*
!.env.exemple
*.pem
*.key
*.p12
id_rsa
id_ed25519
credencials*.json

Aquest bucle de validació en arrencar és més important del que sembla: converteix una fallada silenciosa en producció ("per què no arriben els correus?") en un error immediat i explícit en arrencar.

Nivell 2: gestor de secrets

Per a qualsevol cosa que arribi a producció, el .env al disc d'una màquina es queda curt. Un gestor de secrets és un servei que els emmagatzema xifrats i els lliura sota demanda a qui en té permís.

Avantatge Davant del .env
Xifratge en repòs El .env és text pla al disc
Control d'accés per identitat El .env el llegeix qualsevol que entri a la màquina
Auditoria Registra qui va llegir què i quan
Rotació Pot canviar el valor sense desplegar res
Caducitat Credencials de vida curta, generades al vol
Revocació centralitzada Un sol lloc on tallar l'accés

N'hi ha de diverses famílies: els integrats en cada proveïdor de núvol, els autoallotjats de propòsit general, i els de les mateixes plataformes de CI (els "secrets del repositori", que vam veure fer servir al flux de la lliçó 07-06). Quin triar depèn de la infraestructura; el que importa és el principi.

Un patró intermedi molt utilitzat, quan es vol versionar la configuració xifrada: eines de xifratge de fitxers que desen al repositori un .env.xifrat que només es pot desxifrar amb una clau que és en una altra banda. És acceptable amb dues condicions: que l'algorisme sigui sòlid i que la clau de desxifratge no sigui mai al repositori. I tingues present que si aquesta clau es filtra algun dia, tot l'historial xifrat queda exposat retroactivament.

La regla del mínim privilegi

Independentment d'on siguin, les credencials han de tenir el permís mínim i la vida més curta possible:

  • Un token de CI que només necessita llegir paquets no ha de poder escriure al repositori.
  • Una credencial de desenvolupament mai no ha de servir per a producció.
  • Els tokens amb caducitat són preferibles als permanents, encara que donin més feina.

Així, quan es produeixi una fuita —i se'n produirà—, el dany queda acotat des d'abans.

  1. Detecció: hooks de client i anàlisi al CI

La millor fuita és la que no arriba a confirmar-se. Hi ha tres barreres, per ordre de proximitat al desenvolupador.

Barrera 1: hook pre-commit

Reprenent la lliçó 06-01, un hook que rebutja el commit si detecta patrons sospitosos en allò que està a punt d'entrar:

#!/usr/bin/env bash
# .githooks/pre-commit — detecció bàsica de secrets
# Analitza NOMÉS el que està preparat, que és el que entrarà al commit.

errors=0

# 1. Fitxers que mai no s'han de confirmar, pel nom
patrons_prohibits='(^|/)\.env$|(^|/)\.env\.[^e]|\.pem$|\.p12$|\.pfx$|(^|/)id_rsa$|(^|/)id_ed25519$|credencials.*\.json$'

while IFS= read -r fitxer; do
  if echo "$fitxer" | grep -qE "$patrons_prohibits"; then
    echo "BLOQUEJAT: '$fitxer' sembla que conté credencials." >&2
    errors=1
  fi
done < <(git diff --cached --name-only --diff-filter=ACM)

# 2. Contingut sospitós en allò preparat
#    Patrons genèrics: assignacions de valors llargs a noms reveladors
if git diff --cached -U0 | grep -nE \
   '^\+.*(contrasenya|password|passwd|secret|api[_-]?key|token|private[_-]?key)[[:space:]]*[=:][[:space:]]*["'"'"'][^"'"'"']{12,}' ; then
  echo "" >&2
  echo "BLOQUEJAT: possible credencial al contingut preparat." >&2
  errors=1
fi

# 3. Blocs de clau privada
if git diff --cached | grep -q 'BEGIN [A-Z ]*PRIVATE KEY'; then
  echo "BLOQUEJAT: bloc de clau privada detectat." >&2
  errors=1
fi

# 4. Cadenes de connexió amb credencials incrustades
if git diff --cached -U0 | grep -nE '^\+.*[a-z][a-z0-9+.-]*://[^/[:space:]]+:[^@[:space:]]+@'; then
  echo "BLOQUEJAT: URL amb credencials incrustades." >&2
  errors=1
fi

if [ "$errors" -ne 0 ]; then
  echo "" >&2
  echo "Si és un fals positiu, revisa-ho amb cura abans de fer servir --no-verify." >&2
  echo "Si és real: NO facis servir --no-verify. Treu el valor a una variable d'entorn." >&2
  exit 1
fi

exit 0
chmod +x .githooks/pre-commit
git config --local core.hooksPath .githooks

Hi ha eines especialitzades molt millors que aquest script —amb centenars de patrons específics per proveïdor, càlcul d'entropia i llistes d'exclusió— i s'integren com a hook amb gestors del tipus Husky, tal com vam veure a la lliçó 06-01. L'script de dalt serveix per entendre el mecanisme i com a xarxa mínima.

I l'advertiment de sempre, que la lliçó 07-06 va convertir en principi: --no-verify l'esquiva. Un hook de client no és un control de seguretat. És una ajuda per a la persona honrada amb pressa, que és exactament el perfil que filtra secrets.

Barrera 2: anàlisi al CI

Aquí sí que hi ha control, perquè s'executa on el desenvolupador no mana (lliçó 07-06):

# .github/workflows/ci.yml (fragment)
  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # l'anàlisi necessita l'historial

      - name: Cercar secrets als commits de la PR
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          # Aquí hi aniria l'eina especialitzada de detecció.
          # Com a xarxa mínima, una comprovació de fitxers prohibits:
          if git diff --name-only "$BASE..HEAD" \
             | grep -E '\.env$|\.pem$|id_rsa$|\.p12$'; then
            echo "::error::S'ha afegit un fitxer que sembla que conté credencials"
            exit 1
          fi

      - name: Comprovar que .env.exemple està al dia
        run: |
          # Les claus que el codi llegeix de l'entorn han d'estar documentades
          grep -ohE 'process\.env\.[A-Z_]+' -r . --include='*.js' \
            | sed 's/process\.env\.//' | sort -u > /tmp/usades.txt
          grep -oE '^[A-Z_]+' .env.exemple | sort -u > /tmp/documentades.txt
          if ! comm -23 /tmp/usades.txt /tmp/documentades.txt | grep -q .; then
            echo "Totes les variables estan documentades."
          else
            echo "::error::Falten variables al .env.exemple:"
            comm -23 /tmp/usades.txt /tmp/documentades.txt
            exit 1
          fi

Declarada com a comprovació obligatòria a la branca protegida, aquesta barrera sí que impedeix que el commit arribi a main. Però atenció al matís: el commit ja existeix a la branca publicada. La detecció al CI evita que entri a la línia principal; no evita que el secret ja sigui al servidor. Per això les tres barreres es complementen i no en sobra cap.

Barrera 3: anàlisi periòdica de l'historial complet

Les dues anteriors miren allò nou. De tant en tant convé mirar allò vell, perquè les regles de detecció milloren i perquè l'historial antic es va escriure abans que hi hagués cap barrera:

# Cercar un patró a tot l'historial, a totes les branques
git log --all -p -S 'API_CORREU_CLAU' --oneline

# Cercar al contingut de tots els commits assolibles
git rev-list --all | xargs git grep -n 'BEGIN RSA PRIVATE KEY' 2>/dev/null

# Veure el contingut d'un fitxer en un commit concret
git show 4c8a9f0:config.js

Programa aquesta anàlisi al CI amb una execució setmanal. És barata i troba el que es va colar abans que existissin les regles.

  1. Si ja s'ha filtrat: el procediment per ordre

Ha passat. A gestor-tasques, l'Ana va executar l'anàlisi de l'historial i va trobar això:

git log --all --oneline -- config.js
7c2d9e1 chore: elimina el config.js del repositori
4c8a9f0 chore: configuració inicial de la base de dades
git show 4c8a9f0:config.js
module.exports = {
  baseDades: {
    host: "bd-interna.exemple.cat",
    usuari: "gestor_app",
    contrasenya: "una-clau-ficticia-de-mostra",
  },
  apiCorreu: "clau-de-mostra-no-real",
};

El commit 7c2d9e1 va "eliminar" el fitxer fa sis mesos. El secret continua perfectament accessible. I el repositori fa quatre mesos que és públic.

El procediment té quatre passos i l'ordre no és negociable:

flowchart TD
    A["1. ROTAR la credencial<br/>IMMEDIATAMENT"] --> B["2. Reescriure l'historial<br/>git filter-repo"]
    B --> C["3. Coordinar-se amb l'equip<br/>tots els clons queden obsolets"]
    C --> D["4. Demanar la purga de memòries cau<br/>i assumir que queden còpies"]
    A -.->|"l'única cosa que<br/>de debò neutralitza"| A

La raó de l'ordre és una sola frase, i convé gravar-se-la:

Reescriure l'historial primer és l'error clàssic. Triga hores, requereix coordinar tot l'equip, i durant tot aquest temps la credencial continua sent vàlida. A més, el procés de reescriptura i l'avís a l'equip fan molt de soroll: si algú està observant, li acabes d'assenyalar exactament on ha de mirar. Primer s'invalida el secret; després, amb calma, es neteja.

  1. Pas 1 — Rotar la credencial

Rotar vol dir invalidar el valor filtrat i generar-ne un de nou. És l'única cosa que de debò neutralitza la fuita, perquè és l'única que no depèn de controlar còpies que no controles.

El primer de tot, els primers minuts:

  1. Revoca o canvia la credencial al servei d'origen. No "ja la canviaré quan desplegui": ara. Si és una clau d'API, revoca-la al panell del proveïdor. Si és una contrasenya de base de dades, canvia-la. Si és una clau SSH, elimina la pública autoritzada.
  2. Genera la nova i desa-la on toca (apartat 3), mai al repositori.
  3. Desplega la nova als entorns que la necessitin.
  4. Revisa els registres d'accés del servei des de la data del commit filtrat. És l'única manera de saber si es va fer servir. A l'exemple, des de fa vuit mesos.
  5. Documenta l'incident: què s'ha filtrat, des de quan, què s'ha fet i quan. És imprescindible en un entorn regulat i molt útil en qualsevol.

L'ordre dins del pas 1

Si el servei ho permet, crea primer la credencial nova, desplega-la i revoca la vella després. Així no hi ha tall de servei. Si no ho permet, o si hi ha indicis d'ús indegut, revoca primer i assumeix el tall: una interrupció de deu minuts és infinitament preferible a una credencial viva en un repositori públic.

La conversa incòmoda

Si el repositori ha estat públic, cal assumir el pitjor: la credencial s'ha compromès, independentment que els registres no mostrin res estrany. Els escàners automàtics operen en minuts. Actua com si s'hagués fet servir i revisa què s'hi va poder fer.

I aquí és on l'avís del principi es torna concret: si aquella credencial donava accés a dades personals o a sistemes amb requisits regulatoris, la notificació de la bretxa pot tenir un termini legal. Informa el teu responsable de seguretat o de compliment abans de seguir amb el pas 2. No és un tràmit burocràtic: els terminis comencen a comptar des que es detecta.

  1. Pas 2 — Reescriure l'historial amb git filter-repo

Amb la credencial ja morta, es pot netejar l'historial amb calma. L'objectiu és que el valor filtrat deixi d'existir als objectes del repositori.

L'eina

git filter-repo és l'eina recomanada actualment. Substitueix l'antic git filter-branch, que continua existint però és molt lent i té tants paranys que el mateix Git en desaconsella l'ús a la seva documentació. L'alternativa és BFG Repo-Cleaner, més simple i ràpida per als dos casos habituals (esborrar fitxers i substituir cadenes), encara que menys flexible.

# Instal·lació (varia segons el sistema)
pip install git-filter-repo
# o el paquet de la teva distribució

Abans de començar: dues precaucions

# 1. Còpia de seguretat completa. No és opcional.
cd ~/projectes
cp -r gestor-tasques gestor-tasques-copia-$(date +%Y%m%d)

# 2. Treballa sobre un clon FRESC i sense filtres
#    (filter-repo es nega a operar sobre un repositori "brut")
git clone --mirror ssh://[email protected]/equip/gestor-tasques.git neteja.git
cd neteja.git

--mirror clona totes les referències: branques, etiquetes, notes. És el correcte aquí, perquè el secret pot ser en una branca antiga o en una etiqueta.

Cas A: eliminar un fitxer de tot l'historial

# Elimina config.js de TOTS els commits, a totes les branques
git filter-repo --path config.js --invert-paths
Parsed 847 commits
New history written in 3.21 seconds...
Completely finished after 4.87 seconds.

Es pot aplicar a diversos fitxers o a directoris sencers:

git filter-repo \
  --path config.js \
  --path .env \
  --path credencials/ \
  --invert-paths

Cas B: substituir el valor, conservant el fitxer

Sovint el fitxer ha de continuar existint; el que sobra és el valor. --replace-text pren un fitxer de regles:

cat > /tmp/reemplacaments.txt <<'FI'
una-clau-ficticia-de-mostra==>***ELIMINAT***
clau-de-mostra-no-real==>***ELIMINAT***
regex:contrasenya:\s*"[^"]*"==>contrasenya: "***ELIMINAT***"
FI

git filter-repo --replace-text /tmp/reemplacaments.txt

Format de cada línia: literal==>substitut, o regex:expressió==>substitut. Si s'omet ==>substitut, es fa servir ***REMOVED***.

Aquest fitxer de regles conté els secrets en text pla. Esborra'l tan bon punt acabis:

shred -u /tmp/reemplacaments.txt 2>/dev/null || rm -f /tmp/reemplacaments.txt

Comprovar el resultat

# El fitxer ja no ha d'aparèixer en cap commit
git log --all --oneline -- config.js       # sense sortida

# Ni el valor en cap contingut
git rev-list --all | xargs git grep -n 'una-clau-ficticia' 2>/dev/null   # sense sortida

# Els objectes orfes, fora (vegeu la lliçó 08-06)
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Publicar la reescriptura

filter-repo elimina deliberadament el remot origin com a mesura de seguretat, perquè no puguis publicar sense pensar-hi. Cal tornar-lo a afegir:

git remote add origin ssh://[email protected]/equip/gestor-tasques.git

# Això reescriu TOTES les branques i etiquetes del servidor.
# Requereix treure temporalment la protecció de main.
git push --force --all
git push --force --tags

Aquest és el moment més delicat de tot el procediment. Estàs reescrivint la història publicada de tot el projecte, exactament el que la regla d'or de la lliçó 05-01 prohibeix. Està justificat —és una de les poquíssimes excepcions legítimes—, però exigeix la coordinació del pas 3. Amb --mirror i --force --all, --force-with-lease no s'aplica de la mateixa manera; la protecció aquí és organitzativa: ningú no envia res durant la finestra acordada.

Els efectes que cal assumir

Efecte Detall
Tots els SHA canvien A partir del primer commit modificat. Els enllaços a commits en tiquets, documentació i PR deixen de funcionar
Tots els clons queden obsolets L'Ana, el Bruno, la Carla i el Diego tenen històries divergents
Les PR obertes es trenquen Apunten a commits que ja no existeixen
Les etiquetes es reescriuen Les anotades i signades perden la signatura (apartat 12)
.git-blame-ignore-revs queda obsolet Els SHA que conté ja no existeixen; cal regenerar-lo
El CI pot fallar Memòries cau amb claus basades en SHA que ja no existeixen

  1. Pas 3 — Coordinar-se amb l'equip

Reescriure sense avisar converteix un incident controlat en un caos. La seqüència:

Abans

  1. Anuncia la finestra amb antelació, pel canal que tothom llegeixi.
  2. Que tothom fusioni o tanqui les seves branques. El que quedi obert s'haurà de refer.
  3. Que tothom faci push del que tingui i després pari: ningú no envia res durant la finestra.
  4. Treu temporalment la protecció de main (necessària per al --force), i apunta-t'ho per restaurar-la.

Després: instruccions per a l'equip

L'opció recomanada i més segura és tornar a clonar:

# 1. Desa qualsevol feina pendent fora del repositori
cd ~/projectes/gestor-tasques
git diff > ~/els-meus-canvis-pendents.patch     # per si de cas

# 2. Reanomena el clon vell (encara no l'esborris)
cd ~/projectes
mv gestor-tasques gestor-tasques-VELL

# 3. Clona de nou
git clone ssh://[email protected]/equip/gestor-tasques.git
cd gestor-tasques

# 4. Reconfigura allò local que no viatja (lliçons 08-01 a 08-04)
git config --local core.hooksPath .githooks
git config --local commit.template .gitmissatge
git config --local blame.ignoreRevsFile .git-blame-ignore-revs

# 5. Quan tot funcioni, esborra el vell
rm -rf ~/projectes/gestor-tasques-VELL

Si algú té feina sense publicar que vol rescatar, es rebasa sobre l'historial nou:

# Al clon nou, portar-hi els commits del vell
git remote add vell ~/projectes/gestor-tasques-VELL
git fetch vell
git switch -c GT-155-recuperada vell/GT-155
git rebase --onto main $(git merge-base main vell/GT-155) GT-155-recuperada

Hi pot haver conflictes: els commits que es rebasen es van escriure sobre una base que ja no existeix. Amb branques curtes —l'argument del mòdul 7 a favor d'integrar sovint— el cost és petit; amb una branca de tres setmanes, és dolorós. És un argument més a favor de les branques curtes.

El cas del Diego

El Diego té un fork, és a dir, un repositori independent al servidor. La reescriptura d'origin no toca el seu fork: allà el secret continua intacte, i amb ell a tots els clons del seu fork.

# El Diego, al seu fork (reprenent el triangle de la lliçó 07-01)
git remote -v
origin    ssh://[email protected]/diego/gestor-tasques.git (fetch)
upstream  ssh://[email protected]/equip/gestor-tasques.git (fetch)

Cal demanar-li explícitament que esborri el seu fork i el torni a crear des del repositori ja net, o que hi apliqui la mateixa reescriptura. I el mateix amb qualsevol altre fork existent: en un repositori públic n'hi pot haver desenes que ni tan sols coneixes. És una de les raons per les quals el pas 1 —rotar— és l'única cosa que realment tanca l'assumpte.

Després de tot

  1. Restaura la protecció de main i comprova que ha quedat com estava.
  2. Regenera .git-blame-ignore-revs amb els SHA nous.
  3. Verifica el CI i neteja les memòries cau que depenguin de SHA antics.
  4. Comprova de nou que el secret no hi és: git rev-list --all | xargs git grep ....

  1. Pas 4 — Les còpies que queden a les plataformes

Aquest pas és el que més gent omet, i és el que explica per què el pas 1 no és negociable.

Encara que hagis reescrit l'historial i forçat l'enviament, a les plataformes d'allotjament hi poden quedar còpies accessibles:

On queda Per què
Vista de commits per SHA Moltes plataformes conserven els objectes "orfes" i els serveixen si en coneixes el SHA, de vegades durant molt de temps
Pull requests tancades Desen una còpia dels commits que es van proposar, encara que la branca s'esborri
Comentaris de revisió Citen fragments de codi literalment; si el secret era en una línia comentada, allà continua
Forks Repositoris independents, amb la seva pròpia còpia
Memòries cau i CDN de la plataforma Pàgines i paquets generats, servits des de la memòria cau
Memòries cau del CI Clons i artefactes d'execucions anteriors
Índexs de motors de cerca Si era públic, el contingut pot estar indexat
Serveis d'arxiu de tercers Rèpliques i miralls automàtics de repositoris públics

Què cal fer:

  1. Contacta amb el suport de la plataforma i demana la purga de les referències a la memòria cau i dels objectes orfes. Gairebé totes tenen un procediment per a això; sol requerir una petició explícita.
  2. Revisa i esborra les pull requests que continguin el secret al seu diff o als seus comentaris, si la plataforma ho permet.
  3. Enumera els forks coneguts i demana'n l'eliminació o la neteja.
  4. Neteja les memòries cau del CI.
  5. I sobretot: assumeix que alguna còpia sobreviurà. Sempre.

Aquesta és la raó de fons de l'ordre del procediment. No pots garantir que el secret desaparegui de tot arreu. L'única cosa que sí que controles al 100 % és fer que el valor deixi de servir per a res. Per això rotar és el pas 1, i per això els altres tres passos són higiene important però secundària.

  1. Per què git rm no serveix

Mereix un apartat propi perquè és el malentès més estès i més perillós, i perquè el vam deixar anunciat a la lliçó 08-03.

git rm config.js
git commit -m "chore: elimina el fitxer de configuració amb credencials"
git push

El que ha passat:

  • El fitxer ja no és a la punta de la branca.
  • El fitxer continua al commit 4c8a9f0 i a tots els intermedis.
  • El blob amb el contingut continua a la base d'objectes.
  • Tots els clons el continuen tenint.
# La demostració
git log --all --oneline -- config.js
git show 4c8a9f0:config.js          # allà és, íntegre

Recorda el model de dades de la lliçó 01-04: cada commit apunta a un arbre, i aquell arbre apunta als blobs d'aquella versió. Esborrar el fitxer crea un commit nou amb un arbre que ja no l'inclou. Els arbres anteriors no es modifiquen: són immutables. El blob continua existint, referenciat per ells.

És pitjor que no fer res, per dos motius:

  1. Dóna una falsa sensació de seguretat. "Ja l'he tret" és la frase que fa que ningú no roti la credencial.
  2. Assenyala on cal mirar. Un commit anomenat "elimina el fitxer amb credencials" és una invitació a buscar al commit anterior.
Ordre Treu de la punta Treu de l'historial Neutralitza el secret
git rm No No
git rm --cached Sí (de l'índex) No No
git filter-repo Sí (al teu repositori) No (queden còpies)
Rotar la credencial

L'última fila és la lliçó sencera resumida en una taula.

  1. Autenticació: claus SSH, ssh-agent i gestors de credencials

A la lliçó 04-03 vam veure els mètodes d'autenticació amb el remot i vam deixar pendent la part de seguretat. Aquí és.

Claus SSH amb frase de pas

Una clau SSH sense frase de pas és un fitxer que dóna accés complet al repositori a qualsevol que el copiï. Amb frase de pas, el fitxer tot sol no serveix.

# Generar una clau moderna, amb frase de pas
ssh-keygen -t ed25519 -C "[email protected]"
# Enter passphrase: (escriu una frase llarga, no la deixis buida)

ed25519 és preferible a RSA: més curta, més ràpida i amb seguretat equivalent o millor. Si necessites compatibilitat amb sistemes antics, rsa amb -b 4096.

ssh-agent: la frase de pas una vegada per sessió

L'objecció a la frase de pas és que cal escriure-la cada vegada. ssh-agent la desa en memòria, desxifrada, durant la sessió:

# Arrencar l'agent (normalment ja està en marxa)
eval "$(ssh-agent -s)"

# Afegir-hi la clau: demana la frase UNA vegada
ssh-add ~/.ssh/id_ed25519

# Amb caducitat de 8 hores, que és millor pràctica
ssh-add -t 8h ~/.ssh/id_ed25519

# Veure quines claus té carregades
ssh-add -l

# Descarregar-les totes (en acabar la jornada, o en deixar el portàtil)
ssh-add -D

A macOS, el clauer del sistema la pot desar entre reinicis:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Configuració recomanada a ~/.ssh/config:

Host git.exemple.cat
    User git
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    AddKeysToAgent yes

IdentitiesOnly yes evita que SSH ofereixi totes les teves claus al servidor, cosa que a més prevé bloquejos per excés d'intents.

Permisos dels fitxers

SSH es nega a fer servir una clau amb permisos laxos, i fa bé:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

Gestors de credencials davant de store

Per a HTTPS amb token, la lliçó 04-03 va presentar credential.helper i va advertir sobre store. Ara, la comparació completa:

Ajudant On desa Xifrat Recomanació
store ~/.git-credentials, text pla No Evita'l. Qualsevol procés que llegeixi el teu $HOME obté el token
cache Memòria, amb caducitat No cal (no toca el disc) Acceptable per a un ús puntual
manager (Git Credential Manager) Clauer del sistema Recomanat, multiplataforma
osxkeychain Clauer de macOS Recomanat a macOS
libsecret Clauer de GNOME/KDE Recomanat a Linux d'escriptori
wincred Gestor de credencials de Windows Recomanat a Windows
# Comprovar què tens configurat
git config --get credential.helper

# Si és "store", canvia'l JA
git config --global credential.helper manager       # multiplataforma
git config --global credential.helper osxkeychain   # macOS (Bruno)
git config --global credential.helper libsecret     # Linux (Ana)
git config --global credential.helper manager       # Windows (Carla)

# I revisa si ha quedat res al fitxer de text pla
cat ~/.git-credentials 2>/dev/null   # si té contingut, aquell token està exposat

Si trobes tokens a ~/.git-credentials, rota'ls (pas 1 de l'apartat 6) i esborra el fitxer. Han estat en text pla al teu disc tot aquest temps.

Sobre els tokens d'accés personal

  • Mínim privilegi: només els permisos que necessites.
  • Caducitat curta: renovar-los és una molèstia menor comparada amb un token permanent filtrat.
  • Un per eina: si se'n compromet un, revoques aquell i no tots.
  • Mai a l'URL del remot: https://usuari:[email protected]/... deixa el token a .git/config, en text pla i visible en qualsevol git remote -v que enganxis en un xat.
# Comprovar que no tens cap URL amb credencials incrustades
git config --get-regexp '^remote\..*\.url' | grep '@' | grep -v '^remote.[a-z]*.url ssh://git@'

  1. Signar commits i etiquetes

Hi ha un fet incòmode sobre Git que convé dir amb claredat:

L'autor d'un commit és un camp de text que escrius tu mateix. No hi ha cap verificació.

git -c user.name="Ana Ferrer" -c user.email="[email protected]" \
    commit -m "feat: afegeix una porta del darrere"

Aquest commit apareix a nom de l'Ana al git log, al git blame i a la plataforma. Qualsevol amb permís d'escriptura —o qualsevol que enviï una PR— ho pot fer. La signatura criptogràfica és la resposta.

Amb GPG

# 1. Generar una clau
gpg --full-generate-key
# Tipus: RSA and RSA (o ECC) · Mida: 4096 · Caducitat: 2y
# Nom i correu: els MATEIXOS que a user.name i user.email

# 2. Localitzar-ne l'identificador
gpg --list-secret-keys --keyid-format=long
sec   ed25519/A1B2C3D4E5F6A7B8 2026-08-01 [SC] [caduca: 2028-08-01]
      Empremta = ....
uid   Ana Ferrer <[email protected]>
# 3. Configurar Git
git config --global user.signingkey A1B2C3D4E5F6A7B8
git config --global commit.gpgsign true      # signar SEMPRE
git config --global tag.gpgSign true         # i les etiquetes

# 4. Exportar la clau pública per pujar-la a la plataforma
gpg --armor --export A1B2C3D4E5F6A7B8

Si l'agent de GPG no troba el terminal per demanar la frase de pas:

export GPG_TTY=$(tty)     # afegeix-ho al teu ~/.bashrc o ~/.zshrc

Amb SSH (més simple, Git 2.34+)

Des de Git 2.34 es pot signar amb la mateixa clau SSH que ja fas servir per autenticar-te. És notablement més senzill de muntar:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgSign true

Perquè la verificació local funcioni, cal un fitxer de signants permesos:

cat > ~/.ssh/signants_permesos <<'FI'
[email protected] ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...clau-publica-de-ana
[email protected] ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...clau-publica-de-bruno
[email protected] ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...clau-publica-de-carla
FI

git config --global gpg.ssh.allowedSignersFile ~/.ssh/signants_permesos

Verificar

# Un commit concret
git show --show-signature HEAD

# L'historial amb l'estat de signatura
git log --show-signature -3

# En format compacte: %G? dóna G (bona), B (dolenta), U (desconeguda), N (sense signatura)
git log --pretty="%h %G? %an %s" -10
9f8e7d6 G Ana Ferrer   feat(filtres): afegeix el filtre per etiqueta
7c6b5a4 G Bruno Salas  fix(sync): evita duplicar tasques
4d3c2b1 N Carla Vidal  docs: actualitza el README

La N de la Carla indica que aquell commit no està signat.

I per a les etiquetes, tancant el que vam veure a la lliçó 05-05:

git tag -s v1.5.0 -m "Versió 1.5.0"
git tag -v v1.5.0

Què garanteix una signatura i què no

Garanteix No garanteix
Que qui controla aquella clau va crear el commit Que el codi sigui correcte o segur
Que el contingut no s'ha alterat des de la signatura Que la clau no estigui compromesa
Traçabilitat per a una auditoria Que la persona sigui qui diu ser (això depèn de com es va verificar la clau)
Que un tercer no pugui suplantar un signant Que el commit hagi estat revisat

Les limitacions pràctiques que cal conèixer:

  • Signar no substitueix la revisió. Un commit signat i maliciós continua sent maliciós.
  • Les claus es comprometen. Per això caduquen i per això existeixen els certificats de revocació (genera el teu en crear la clau i desa'l en un lloc segur).
  • Reescriure l'historial destrueix les signatures. Un rebase o un filter-repo genera commits nous, sense signatura. És un efecte col·lateral del pas 2 de l'apartat 7 que cal anticipar.
  • El condensat perd les signatures dels commits originals. Enllaça amb la política d'integració de la lliçó 08-02: si la signatura és un requisit d'auditoria, el condensat del servidor signat per la plataforma no és el mateix que els commits signats pels seus autors.
  • Exigir signatura als col·laboradors externs té un cost. Al Diego li hauríem de demanar que munti GPG o SSH signing abans de la seva primera PR. És raonable en un projecte amb requisits d'auditoria i una barrera innecessària en un projecte petit.

Es pot exigir la signatura a les branques protegides de la plataforma, al costat de les comprovacions de la lliçó 07-06.

  1. Higiene: permisos, branques protegides i --force

Permisos

  • Mínim privilegi també per a les persones. El Diego no necessita permís d'escriptura: treballa des del seu fork i això és exactament el correcte (lliçó 07-01).
  • Revisa els accessos periòdicament. La gent canvia d'equip i d'empresa; els permisos rarament es retiren sols. Amb una revisió trimestral n'hi ha prou.
  • Compte amb els comptes de servei i els tokens de desplegament. Solen tenir permisos amplis i no caducar mai, i ningú no els revisa perquè "funcionen".
  • Les aplicacions i integracions de tercers connectades al repositori també hi tenen accés. Audita què hi ha connectat i retira el que no es faci servir.

Branques protegides

Reprenent la lliçó 07-06, una branca protegida ben configurada evita accidents i atacs:

Regla Què prevé
Prohibir el push directe Que algú se salti la revisió
Exigir PR amb aprovacions Que un canvi entri sense que ningú se'l miri
Exigir comprovacions en verd Que entri codi que no compila o amb secrets detectats
Prohibir --force Que es reescrigui l'historial publicat
Prohibir l'esborrat de la branca Un accident irreversible
Exigir commits signats Suplantació d'autoria
Exigir que la branca estigui al dia Conflictes semàntics

--force

Recorda de la lliçó 04-05: --force sobreescriu el que hi hagi al servidor sense mirar. Si el Bruno va enviar alguna cosa mentre tu treballaves, desapareix.

# MAI per costum
git push --force

# SEMPRE això al seu lloc
git push --force-with-lease

--force-with-lease comprova que l'estat remot és el que tu et pensaves; si algú hi ha enviat alguna cosa, l'operació es rebutja. Ho pots fer permanent amb un àlies:

git config --global alias.pushf 'push --force-with-lease'

L'única excepció legítima al --force pur és la de l'apartat 7: la reescriptura coordinada després d'una filtració, amb la finestra acordada i tothom aturat.

Un detall que s'oblida: les dades de l'autor

git log -1 --pretty="%an <%ae>"

Si treballes en un projecte públic amb el teu correu corporatiu, o a l'inrevés, revisa què estàs publicant. Pots fer servir configuració per directori (lliçó 01-05):

# ~/.gitconfig
[includeIf "gitdir:~/projectes/feina/"]
    path = ~/.gitconfig-feina
[includeIf "gitdir:~/projectes/personal/"]
    path = ~/.gitconfig-personal

  1. Abans de fer públic un repositori

Fer públic un repositori privat és una operació irreversible a la pràctica: encara que el tornis a privat en cinc minuts, cal assumir que algú l'ha clonat. La llista de comprovació:

# 1. Cercar patrons de secrets a TOT l'historial
git rev-list --all | xargs git grep -nE \
  '(contrasenya|password|passwd|secret|api[_-]?key|token)[[:space:]]*[=:]' 2>/dev/null | head -50

# 2. Blocs de clau privada
git rev-list --all | xargs git grep -n 'BEGIN [A-Z ]*PRIVATE KEY' 2>/dev/null

# 3. URL amb credencials incrustades
git rev-list --all | xargs git grep -nE '[a-z]+://[^/[:space:]]+:[^@[:space:]]+@' 2>/dev/null

# 4. Fitxers sospitosos que hagin existit alguna vegada
git log --all --pretty=format: --name-only --diff-filter=A \
  | sort -u | grep -iE '\.env|\.pem$|\.key$|id_rsa|credencial|secret'

# 5. Els fitxers més grans (sovint són bolcats de dades)
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" {print $3, $4}' | sort -rn | head -20

I la revisió manual:

  • [ ] Hi ha correus personals o adreces internes als missatges de commit?
  • [ ] Hi ha noms de host, IP internes o rutes de xarxa a la configuració?
  • [ ] Hi ha bolcats de base de dades amb dades reals?
  • [ ] Hi ha captures de pantalla amb dades identificables?
  • [ ] Hi ha comentaris de codi que revelin vulnerabilitats conegudes i sense corregir?
  • [ ] El README.md descriu infraestructura interna?
  • [ ] Hi ha fitxers de configuració de CI amb noms de secrets que donin pistes?
  • [ ] La llicència és la correcta i hi ha el fitxer LICENSE?

En un entorn regulat, aquesta llista de comprovació no la signa qui ha escrit el codi. L'ha de revisar el responsable de seguretat o de compliment abans de publicar. No és burocràcia: és l'única manera que algú amb un criteri diferent es miri el mateix.

Errors Habituals i Consells

Error 1: creure que git rm elimina un secret. És l'error central de la lliçó. Només el treu de la punta; l'historial i tots els clons el conserven.

Error 2: reescriure l'historial abans de rotar. És l'ordre invers al correcte. La reescriptura triga hores, fa soroll i durant tot aquest temps la credencial continua viva. Rotar primer, sempre.

Error 3: no rotar perquè "els registres no mostren accessos estranys". L'absència d'evidència no és evidència d'absència. Si va estar en un repositori públic, es considera compromesa.

Error 4: reescriure sense avisar l'equip. Tots els clons queden divergents, les PR obertes es trenquen i algú acabarà reintroduint l'historial vell amb un push des del seu clon antic.

Error 5: oblidar els forks. La reescriptura d'origin no toca el fork del Diego. Si el repositori és públic, hi pot haver forks que ni tan sols coneixes.

Error 6: continuar amb credential.helper store. Desa els tokens en text pla al teu $HOME. Canvia'l pel gestor del teu sistema i rota el que hi hagués a dins.

Error 7: claus SSH sense frase de pas. El fitxer tot sol dóna accés complet. ssh-agent elimina la incomoditat d'escriure-la.

Error 8: token a l'URL del remot. Queda a .git/config en text pla i apareix en qualsevol git remote -v que algú enganxi en un xat.

Error 9: valors reals al .env.exemple. Trenca tot el patró: el fitxer versionat passa a ser el que té el secret.

Error 10: confiar en el hook pre-commit com a control. --no-verify l'esquiva. És ajuda, no control; el control és al CI sobre branca protegida.

Consell 1: fes l'anàlisi de l'historial complet avui. Les ordres de l'apartat 14 triguen segons. Pot ser que hi trobis alguna cosa de fa anys.

Consell 2: mínim privilegi i caducitat curta a totes les credencials. Quan es produeixi la fuita —es produirà—, el dany estarà acotat des d'abans.

Consell 3: munta la detecció abans de necessitar-la. Un hook pre-commit més una anàlisi al CI costen una tarda i eviten el procediment de quatre passos.

Consell 4: documenta el procediment d'incident al README.md. El dia que passi, ningú no tindrà el cap prou fred per improvisar-lo. Escriu els quatre passos i a qui cal avisar.

Consell 5: signa els teus commits. Amb SSH costa tres ordres i dóna traçabilitat real d'autoria.

Consell 6: --force-with-lease sempre, amb àlies. Elimina tota una categoria d'accidents.

Consell 7: en entorns regulats, avisa abans d'actuar. Els terminis de notificació comencen en detectar, no en acabar la neteja.

Exercicis

Exercici 1: trobar el secret a l'historial

Munta l'escenari de gestor-tasques i practica la detecció:

  1. Crea un repositori i confirma un config.js amb una contrasenya fictícia.
  2. Afegeix-hi tres commits més de feina normal.
  3. "Elimina" el fitxer amb git rm i confirma.
  4. Demostra, amb tres ordres diferents, que la contrasenya continua sent accessible.
  5. Explica, recolzant-te en el model de dades de la lliçó 01-04, per què encara hi és.

Exercici 2: el procediment complet

Partint del repositori de l'exercici 1:

  1. Enumera per escrit els quatre passos del procediment per ordre, i justifica per què rotar va primer.
  2. Fes una còpia de seguretat del repositori.
  3. Elimina config.js de tot l'historial amb git filter-repo --path ... --invert-paths.
  4. Verifica que ja no apareix amb les tres ordres de l'exercici 1.
  5. Repeteix l'exercici amb --replace-text, substituint només el valor i conservant el fitxer.
  6. Compara els SHA abans i després. Què implica per a l'equip? I per al .git-blame-ignore-revs?
  7. Enumera quines còpies del secret continuarien existint encara que el procés sortís perfecte.

Exercici 3: signatura i verificació

  1. Configura la signatura de commits amb SSH (gpg.format=ssh), fent servir una clau nova creada per a l'exercici.
  2. Crea el fitxer de signants permesos i configura gpg.ssh.allowedSignersFile.
  3. Fes un commit signat i un altre sense signar (amb --no-gpg-sign).
  4. Mostra l'historial amb %G? i distingeix-los tots dos.
  5. Signa una etiqueta anotada i verifica-la amb git tag -v.
  6. Executa git rebase -i HEAD~2 amb un reword i comprova què li passa a la signatura. Explica'n la relació amb el pas 2 del procediment de filtració.

Solucions

Solució 1:

mkdir /tmp/practica-seg && cd /tmp/practica-seg && git init -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"

cat > config.js <<'FI'
module.exports = {
  baseDades: {
    host: "bd-interna.exemple.cat",
    usuari: "gestor_app",
    contrasenya: "una-clau-ficticia-de-mostra",
  },
};
FI
echo "console.log('gestor-tasques');" > app.js
git add . && git commit -m "chore: configuració inicial de la base de dades"
# 2. Feina normal a sobre
for i in 1 2 3; do
  echo "// canvi $i" >> app.js
  git commit -am "feat(app): canvi $i"
done

# 3. "Eliminar" el fitxer
git rm config.js && git commit -m "chore: elimina el config.js del repositori"
ls    # config.js ja no és al disc
# 4a. L'historial del fitxer
git log --all --oneline -- config.js
7c2d9e1 chore: elimina el config.js del repositori
4c8a9f0 chore: configuració inicial de la base de dades
# 4b. El contingut en un commit concret
git show 4c8a9f0:config.js
module.exports = {
  baseDades: {
    host: "bd-interna.exemple.cat",
    usuari: "gestor_app",
    contrasenya: "una-clau-ficticia-de-mostra",
  },
};
# 4c. Cerca cega per contingut a tot l'historial
git rev-list --all | xargs git grep -n 'una-clau-ficticia' 2>/dev/null
4c8a9f0:config.js:5:    contrasenya: "una-clau-ficticia-de-mostra",
# Extra: el blob continua sent un objecte viu
git rev-parse 4c8a9f0:config.js          # el SHA del blob
git cat-file -p $(git rev-parse 4c8a9f0:config.js)

5. Per què encara hi és. Segons el model de dades de la lliçó 01-04, un commit apunta a un arbre, i aquell arbre llista els blobs d'aquella versió del projecte. Els objectes de Git són immutables: el seu SHA és el hash del seu contingut, o sigui que modificar-los és impossible per construcció.

git rm no modifica res existent: crea un commit nou amb un arbre que ja no inclou config.js. Els arbres de 4c8a9f0 i dels tres commits següents continuen exactament igual, continuen llistant el blob amb la contrasenya, i aquell blob continua sent assolible des de main recorrent l'historial cap enrere. Mentre un commit assolible el referenciï, git gc no l'eliminarà mai.

L'única manera que desaparegui és reescriure tots els commits des del primer que el contenia, generant arbres nous sense ell. Això canvia els seus SHA i, en cascada, els de tots els seus descendents. És exactament el que fa git filter-repo, i és la raó que sigui una operació tan disruptiva.

Solució 2:

1. Els quatre passos:

# Pas Per què en aquest ordre
1 Rotar/revocar la credencial És l'única cosa que la neutralitza de debò. No depèn de controlar còpies que no controles. És ràpid: minuts
2 Reescriure l'historial (git filter-repo) Neteja el teu repositori. Triga hores amb la coordinació, i durant aquest temps la credencial vella ja no serveix
3 Coordinar-se amb l'equip Tots els clons queden divergents; sense això, algú reintrodueix l'historial vell
4 Demanar la purga a la plataforma Queden còpies a PR, memòries cau i forks. És l'últim perquè és el que menys controles

Rotar va primer per tres raons acumulatives: (a) és l'única cosa que funciona amb seguretat, ja que no pots garantir l'eliminació de totes les còpies; (b) és el més ràpid, mentre que la reescriptura requereix coordinar tot l'equip; i (c) la reescriptura fa molt de soroll —avisos a l'equip, commits que canvien, PR trencades— i si algú està observant, li assenyales exactament on ha de mirar mentre la credencial continua viva.

# 2. Còpia de seguretat
cd /tmp && cp -r practica-seg practica-seg-copia

# 3. Eliminar el fitxer de tot l'historial
cd /tmp/practica-seg
git filter-repo --path config.js --invert-paths --force
Parsed 5 commits
New history written in 0.03 seconds...
Completely finished after 0.11 seconds.
# 4. Verificació
git log --all --oneline -- config.js                            # sense sortida
git rev-list --all | xargs git grep -n 'una-clau' 2>/dev/null   # sense sortida
git show 4c8a9f0:config.js
fatal: invalid object name '4c8a9f0'

El commit ni tan sols existeix ja: es va reescriure amb un altre SHA.

# 5. La variant que conserva el fitxer
cd /tmp && rm -rf practica-seg2 && cp -r practica-seg-copia practica-seg2 && cd practica-seg2

cat > /tmp/reemplacaments.txt <<'FI'
una-clau-ficticia-de-mostra==>***ELIMINAT***
FI

git filter-repo --replace-text /tmp/reemplacaments.txt --force
rm -f /tmp/reemplacaments.txt   # el fitxer de regles contenia el secret

git log --all --oneline -- config.js     # el fitxer SÍ que continua a l'historial
git rev-list --all | xargs git grep -n 'ELIMINAT' 2>/dev/null
a9f8e7d:config.js:5:    contrasenya: "***ELIMINAT***",

El fitxer es conserva amb tota la seva estructura; només el valor ha desaparegut de totes les versions.

# 6. Comparar els SHA
cd /tmp/practica-seg-copia && git log --oneline
cd /tmp/practica-seg && git log --oneline

Tots els SHA són diferents, des del primer commit reescrit fins a la punta. Implicacions:

  • Per a l'equip: tots els clons tenen una història divergent. Un git pull produiria un embolic enorme. Cal tornar a clonar i refer les branques vives (pas 3).
  • Per al .git-blame-ignore-revs: conté SHA que ja no existeixen, o sigui que queda inservible i cal regenerar-lo amb els nous. El mateix passa amb qualsevol SHA citat en tiquets, documentació, comentaris de PR o memòries cau del CI.
  • Per a les signatures (apartat 12): els commits reescrits perden la signatura, perquè són objectes nous.

7. Còpies que continuarien existint encara que tot sortís perfecte:

  • Els clons de l'Ana, el Bruno i la Carla, fins que tornin a clonar.
  • El fork del Diego, que és un repositori independent que la reescriptura d'origin no toca.
  • Els clons que altres persones hagin fet del fork del Diego.
  • Les pull requests tancades a la plataforma, amb els seus diffs.
  • Els comentaris de revisió que citessin aquelles línies.
  • Les memòries cau de la plataforma i els objectes orfes accessibles per SHA.
  • Les memòries cau i els artefactes del CI.
  • Les còpies de seguretat del servidor.
  • Si va ser públic: índexs de cercadors, serveis d'arxiu i escàners automàtics.

I aquí hi ha la conclusió de la lliçó: el pas 2 neteja el teu repositori, no el món. Per això el pas 1 és l'únic no negociable.

Solució 3:

mkdir /tmp/practica-signatura && cd /tmp/practica-signatura && git init -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"

# 1. Clau nova només per a l'exercici
ssh-keygen -t ed25519 -f /tmp/clau-signatura -N "" -C "[email protected]"

git config --local gpg.format ssh
git config --local user.signingkey /tmp/clau-signatura.pub
git config --local commit.gpgsign true
git config --local tag.gpgSign true
# 2. Signants permesos
echo "[email protected] $(cat /tmp/clau-signatura.pub)" > /tmp/signants
git config --local gpg.ssh.allowedSignersFile /tmp/signants
# 3. Un commit signat i un altre sense signar
echo "un" > app.js && git add . && git commit -m "feat: primer commit signat"
echo "dos" >> app.js && git commit -am "feat: commit sense signar" --no-gpg-sign
# 4. Distingir-los
git log --pretty="%h %G? %an %s"
b2c3d4e N Ana Ferrer feat: commit sense signar
a1b2c3d G Ana Ferrer feat: primer commit signat

G = signatura bona i verificada. N = sense signatura. També hi ha B (signatura incorrecta) i U (signatura bona, signant no fiable).

git show --show-signature a1b2c3d | head -5
commit a1b2c3d...
Good "git" signature for [email protected] with ED25519 key SHA256:...
# 5. Etiqueta signada
git tag -s v1.0.0 -m "Primera versió"
git tag -v v1.0.0
object a1b2c3d...
type commit
tag v1.0.0
...
Good "git" signature for [email protected] with ED25519 key SHA256:...
# 6. Què passa en reescriure
git rebase -i HEAD~2      # canvia "pick" per "reword" al primer, desa i edita el missatge
git log --pretty="%h %G? %an %s"
d4e5f6a N Ana Ferrer feat: commit sense signar
c3d4e5f G Ana Ferrer feat: primer commit signat (reescrit)

Els SHA han canviat. Amb commit.gpgsign true, Git torna a signar els commits reescrits amb la teva clau. Si aquella configuració estigués desactivada, o si estiguessis reescrivint commits d'altres persones, totes les signatures es perdrien: els commits nous serien teus i sense signatura.

La relació amb el pas 2 del procediment de filtració és directa i cal anticipar-la: git filter-repo reescriu tots els commits afectats, generant objectes nous. Les signatures originals no sobreviuen, perquè una signatura cobreix el contingut exacte d'un objecte concret i aquell objecte ja no existeix. Després de netejar un secret:

  • Es perd la traçabilitat criptogràfica d'autoria de tot l'historial reescrit.
  • Les etiquetes signades de versions anteriors deixen de verificar-se.
  • Si la signatura és un requisit d'auditoria, cal documentar l'incident i la reescriptura com a part de l'expedient, perquè l'evidència criptogràfica anterior ja no es pot reconstruir.

És un cost real de la neteja, i una altra raó —a més de totes les anteriors— per invertir en prevenció: les barreres de l'apartat 4 són moltíssim més barates que aquest procediment.

Conclusió

L'essencial d'aquesta lliçó:

  • Un repositori és el pitjor lloc possible per a un secret: es clona sencer, és immutable per disseny, es replica sense control i és trivial de cercar. D'aquí ve la idea que ho governa tot: l'única acció que neutralitza un secret filtrat és invalidar-lo.
  • És secret tot allò que caduca, es rota o es pot revocar, més les dades personals i la informació d'infraestructura. En cas de dubte, és secret.
  • La gestió correcta treu els valors del repositori: variables d'entorn amb .env ignorat i .env.exemple versionat per a allò petit, gestor de secrets per a allò que arriba a producció, i mínim privilegi i caducitat curta sempre.
  • La detecció té tres barreres: el hook pre-commit (ajuda, no control: --no-verify existeix), l'anàlisi al CI sobre branca protegida (això sí que és control) i l'anàlisi periòdica de l'historial complet.
  • Si ja s'ha filtrat, l'ordre no és negociable:
    1. Rotar la credencial immediatament. És l'única cosa que de debò funciona, i és el més ràpid.
    2. Reescriure l'historial amb git filter-repo (o BFG), sobre un clon --mirror, amb còpia de seguretat prèvia.
    3. Coordinar-se amb l'equip: tots els clons queden obsolets, cal tornar a clonar i refer les branques; i els forks són repositoris a part que no es netegen sols.
    4. Demanar la purga a la plataforma i assumir que queden còpies a PR tancades, comentaris, memòries cau i forks.
  • git rm no esborra de l'historial. Només treu el fitxer de la punta; el blob continua viu, assolible i present a tots els clons. És pitjor que no fer res, perquè dóna falsa seguretat i assenyala on cal mirar.
  • En autenticació, tancant la lliçó 04-03: claus SSH ed25519 amb frase de pas, ssh-agent amb caducitat perquè no molesti, i gestor de credencials del sistema en lloc de credential.helper store, que desa els tokens en text pla. Mai credencials a l'URL del remot.
  • Signar commits i etiquetes (GPG o, més senzill, gpg.format=ssh) dóna traçabilitat criptogràfica d'autoria, perquè el camp d'autor d'un commit és text lliure. Garanteix qui va signar i que el contingut no s'ha alterat; no garanteix que el codi sigui correcte ni que la clau no estigui compromesa. I la reescriptura de l'historial destrueix les signatures.
  • Higiene: mínim privilegi també per a les persones, revisió periòdica d'accessos, branques protegides ben configurades, --force-with-lease sempre, i una llista de comprovació abans de fer públic un repositori.
  • I l'avís de fons: en entorns regulats, avisa el teu responsable de seguretat o de compliment abans d'actuar. Els terminis de notificació comencen en detectar, no en acabar de netejar.

gestor-tasques ja té missatges útils, historial net, els fitxers correctes, tractats com toca, i els seus secrets a fora. Queda un últim assumpte del mòdul, molt menys dramàtic però cada cop més molest: el repositori s'ha tornat lent. El git status triga tres segons a la màquina de la Carla, el clon pesa 900 MB tot i que el codi n'ocupa 4, i ningú no sap gaire bé per què.

Ho diagnostiquem i ho arreglem a la lliçó 08-06: Consells de Rendiment.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats