Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@ skeletons. Default region: **eu-west-3** (Paris) 🇫🇷.
- An AWS account with permission to create the resources above (Aurora DSQL is available
in eu-west-3 among others).
- **Docker** installed and **running** (buildx; the image is `linux/arm64`).
- **Terraform ≥ 1.14** (an older 1.5.x on your PATH will fail the `required_version` check).
- **Terraform ≥ 1.14**.
- **AWS CLI v2** with an **active session** for your profile — `aws sso login --profile <p>`
or exported credentials. The principal must be able to create DSQL, S3, ECR, DynamoDB,
SSM, Lambda, API Gateway, CloudFront and IAM roles.
- **make**, `git`, `curl`, `unzip`.
- Aurora DSQL, Bedrock etc. beyond the above are **not** used by the bare platform.

## Quick start

Expand Down Expand Up @@ -94,12 +93,13 @@ make run-local # SPIP on http://localhost:8080 (Apache)

| Task | Doc |
|---|---|
| Add a plugin (third-party or custom) | [docs/plugins.md](docs/plugins.md) |
| Upgrade SPIP core | [docs/spip-upgrade.md](docs/spip-upgrade.md) |
| Add / configure an environment | [docs/environments.md](docs/environments.md) |
| Initialise the database + admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) |
| Reset an admin password | [docs/spip-passwords.md](docs/spip-passwords.md) |
| DSQL, sessions, S3, secrets, tracing, logging, cron | `docs/*.md` |
| Add a plugin (third-party or custom) | [docs/plugins.md](docs/en/plugins.md) |
| Upgrade SPIP core | [docs/spip-upgrade.md](docs/en/spip-upgrade.md) |
| Build the image / Composer deps / shrink-vendor | [docs/build.md](docs/en/build.md) |
| Add / configure an environment | [docs/environments.md](docs/en/environments.md) |
| Initialise the database + admin | [docs/db-bootstrap.md](docs/en/db-bootstrap.md) |
| Reset an admin password | [docs/spip-passwords.md](docs/en/spip-passwords.md) |
| DSQL, sessions, S3, secrets, tracing, logging, cron | `docs/en/*.md` |

## Security notes

Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@ plugins et vos squelettes. Région par défaut : **eu-west-3** (Paris) 🇫🇷.
- Un compte AWS autorisé à créer les ressources ci-dessus (Aurora DSQL est disponible en
eu-west-3, entre autres).
- **Docker** installé et **démarré** (buildx ; l'image est `linux/arm64`).
- **Terraform ≥ 1.14** (un vieux 1.5.x dans le PATH échoue sur le contrôle `required_version`).
- **Terraform ≥ 1.14**.
- **AWS CLI v2** avec une **session active** pour votre profil — `aws sso login --profile <p>`
ou des credentials exportés. Le principal doit pouvoir créer DSQL, S3, ECR, DynamoDB, SSM,
Lambda, API Gateway, CloudFront et des rôles IAM.
- **make**, `git`, `curl`, `unzip`.
- Au-delà, Aurora DSQL, Bedrock, etc. ne sont **pas** utilisés par la plateforme nue.

## Démarrage rapide

Expand Down Expand Up @@ -94,14 +93,15 @@ make run-local # SPIP sur http://localhost:8080 (Apache)

| Tâche | Doc |
|---|---|
| Ajouter un plugin (tiers ou maison) | [docs/plugins.md](docs/plugins.md) |
| Mettre à jour le cœur SPIP | [docs/spip-upgrade.md](docs/spip-upgrade.md) |
| Ajouter / configurer un environnement | [docs/environments.md](docs/environments.md) |
| Initialiser la base + l'admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) |
| Réinitialiser un mot de passe admin | [docs/spip-passwords.md](docs/spip-passwords.md) |
| DSQL, sessions, S3, secrets, tracing, logs, cron | `docs/*.md` |

> Les docs détaillées sous `docs/*.md` sont en anglais pour l'instant.
| Ajouter un plugin (tiers ou maison) | [docs/plugins.md](docs/fr/plugins.md) |
| Mettre à jour le cœur SPIP | [docs/spip-upgrade.md](docs/fr/spip-upgrade.md) |
| Build de l'image / dépendances Composer / shrink-vendor | [docs/build.md](docs/fr/build.md) |
| Ajouter / configurer un environnement | [docs/environments.md](docs/fr/environments.md) |
| Initialiser la base + l'admin | [docs/db-bootstrap.md](docs/fr/db-bootstrap.md) |
| Réinitialiser un mot de passe admin | [docs/spip-passwords.md](docs/fr/spip-passwords.md) |
| DSQL, sessions, S3, secrets, tracing, logs, cron | `docs/fr/*.md` |

> Les docs détaillées sous `docs/fr/*.md` sont en anglais pour l'instant.

## Notes de sécurité

Expand Down
77 changes: 77 additions & 0 deletions docs/en/build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Build & image internals

[Français](../fr/build.md) · **English**

The SPIP runtime is a single **immutable, multi-stage Docker image** (ARM64, PHP 8.5 via
Bref). `make build` builds it; `make deploy` builds + pushes it to ECR and points the
Lambda at the new digest. Nothing is installed at runtime — everything is baked here.

## Dockerfile stages (`spip/Dockerfile`)

| Stage | Role |
|---|---|
| `spip-core` | Downloads the official SPIP release zip for `SPIP_VERSION` into `/spip-src` (core is **not** vendored — see [spip-upgrade.md](spip-upgrade.md)). |
| `ext-pgsql` | Compiles the PHP `pgsql` extension — Aurora DSQL speaks the PostgreSQL wire protocol. |
| `ext-gd` | Compiles the PHP `gd` extension (image resizing/thumbnails). |
| `vendors` | Runs Composer **on top of** the fetched SPIP core, then shrinks `vendor/`. |
| `local` | Apache image for `make run-local` (local dev only). |
| `lambda` | Final image: assembles core + vendor + extensions + overlays + plugins, patches `documents.php` for S3, pre-warms opcache, adds the ADOT collector. |

## Composer dependencies (the `vendors` stage)

SPIP core ships its own `vendor/` in the release zip. On top of it we add the pieces that
make SPIP run on Lambda:

```
composer require \
bref/bref:^3.0 # PHP runtime for Lambda (FPM)
bref/secrets-loader:^1 # resolves bref-ssm:/... env values from SSM at boot
aws/aws-sdk-php:^3.0 # DSQL IAM token, S3, DynamoDB sessions, SSM, CloudFront…
open-telemetry/sdk:^1 # tracing
open-telemetry/exporter-otlp:^1
```

Then `spip/scripts/shrink-vendor.sh vendor` runs (see below).

## `shrink-vendor.sh` — why and what

The full `aws/aws-sdk-php` ships data + client classes for **every** AWS service (~hundreds
of MB). A Lambda image only needs a handful, and a smaller image means faster cold starts
and cheaper storage. The script:

- **Keeps only the AWS SDK services SPIP uses** (a `KEEP_SERVICES` allow-list) and deletes
the rest of `aws-sdk-php/src/data/*` and `src/<Service>/`.
- **Strips docs/tests/examples** from the whole `vendor/` tree (`*.md`, `CHANGELOG*`,
`LICENSE*`, `README*`, `tests/`, `docs/`, `examples/`).
- Prints the saved size (`AWS SDK: <before>MB → <after>MB`).

> Note: the current `KEEP_SERVICES` allow-list still contains a few services inherited from
> the original application (`bedrock-*`, `translate`) that the bare platform does not use.
> Trim them to `dsql, dynamodb, s3, ssm, sts, cloudfront, ses` for a leaner image, and add
> back whatever your own plugins need.

To add an SDK service your plugin needs: add it to `KEEP_SERVICES` (and to the `src/`
`case` allow-list) in `spip/scripts/shrink-vendor.sh`, then rebuild.

## Opcache pre-warm

`compile-opcache.php` (run with `opcache-build.ini`) compiles the PHP files into a
read-only opcache **file cache** baked into the image (`/bref/opcache`), read at runtime.
This removes first-request compilation from the cold-start path.

## Observability bits

- **ADOT collector**: `make download-adot` fetches the AWS OTEL collector Lambda extension
into `spip/overlay/adot-collector` (git-ignored); the image ships it as a `/opt`
extension. Traces flow OTEL → collector → X-Ray. See [tracing.md](tracing.md).
- **Logs**: `logs_stderr` sends `spip_log()` to stderr → CloudWatch. See [logging.md](logging.md).

## Where to change what

| You want to… | Edit |
|---|---|
| Bump SPIP | `spip/SPIP_VERSION` (see [spip-upgrade.md](spip-upgrade.md)) |
| Add a PHP/Composer dependency | the `composer require` line in `spip/Dockerfile` |
| Keep another AWS SDK service | `KEEP_SERVICES` in `spip/scripts/shrink-vendor.sh` |
| Add a PHP extension | a new `ext-*` stage in `spip/Dockerfile` |
| Add a plugin | see [plugins.md](plugins.md) |
2 changes: 2 additions & 0 deletions docs/cron.md → docs/en/cron.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Cron (SPIP Job Queue)

[Français](../fr/cron.md) · **English**

## Problem

SPIP's job queue (`genie`) normally runs inline on every web request — it checks for pending jobs and executes them. On Lambda, this adds latency to every request and is unreliable (Lambda instances are ephemeral).
Expand Down
8 changes: 5 additions & 3 deletions docs/db-bootstrap.md → docs/en/db-bootstrap.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Bootstrap the DSQL database for a new environment (SPIP)

[Français](../fr/db-bootstrap.md) · **English**

How to **initialise the SPIP schema** (the `spip_*` tables) on an empty Aurora DSQL
cluster — once per environment (test / prep / prod).

Expand All @@ -22,7 +24,7 @@ across accounts, no web wizard, no need to undo the baked `connect.php`.
3. `actualise_plugins_actifs()` + `plugin_installes_meta()` — runs each active plugin's
`*_upgrade()` (your plugin migrations, plus any bundled ones)
4. creates the admin author (`0minirezo`, webmestre) with a **peppered** hash via
`Spip\Chiffrer\Password::hacher` (see `docs/spip-passwords.md`)
`Spip\Chiffrer\Password::hacher` (see `docs/en/spip-passwords.md`)

## Run it (per environment)

Expand Down Expand Up @@ -88,8 +90,8 @@ status done
- `spip/scripts/bootstrap-db.php` — the CLI script
- `spip/overlay/config/connect.php` — dynamic DSQL connection (IAM token)
- `spip/overlay/php/prepend.php` — writes `cles.php` from `SPIP_CLES`, sets `_DIR_*`
- `docs/spip-passwords.md` — peppered admin author hash
- `docs/dsql.md` — psql connection to the cluster via IAM token
- `docs/en/spip-passwords.md` — peppered admin author hash
- `docs/en/dsql.md` — psql connection to the cluster via IAM token

## Technical notes (pitfalls solved)

Expand Down
2 changes: 2 additions & 0 deletions docs/dsql.md → docs/en/dsql.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Aurora DSQL Driver

[Français](../fr/dsql.md) · **English**

## Overview

SPIP uses a custom PostgreSQL driver (`dsql.php`) to connect to Aurora DSQL. It's a fork of SPIP's experimental PostgreSQL driver (`ecrire/req/pg.exp.php`) with DSQL-specific adaptations.
Expand Down
10 changes: 6 additions & 4 deletions docs/environments.md → docs/en/environments.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Environments

[Français](../fr/environments.md) · **English**

An environment = one AWS account (or one account + region) running its own copy of the
two Terraform stacks and the SPIP image. The default region is **eu-west-3** (Paris).

Expand Down Expand Up @@ -44,20 +46,20 @@ Fill in:
> If you change `domain_name` on an already-bootstrapped environment, the runtime host
> (prepend.php / `SPIP_PUBLIC_URL`) follows automatically, but the stored `adresse_site`
> meta does not — re-run the bootstrap (or `UPDATE spip_meta SET valeur='https://<new>'
> WHERE nom='adresse_site'`). See `docs/db-bootstrap.md`.
> WHERE nom='adresse_site'`). See `docs/en/db-bootstrap.md`.

## First bring-up (per environment)

```bash
# 1. static stack (DSQL, S3, ECR, DynamoDB, SSM)
make deploy-static ENV=prod AWS_PROFILE=<profile>

# 2. fill the SPIP key material placeholder in SSM (see docs/db-bootstrap.md)
# 2. fill the SPIP key material placeholder in SSM (see docs/en/db-bootstrap.md)

# 3. build + push image, sync assets, apply app stack
make deploy ENV=prod AWS_PROFILE=<profile>

# 4. initialise the SPIP schema + admin author (see docs/db-bootstrap.md)
# 4. initialise the SPIP schema + admin author (see docs/en/db-bootstrap.md)
```

## CI/CD
Expand All @@ -67,4 +69,4 @@ environment under **GitHub → Settings → Environments** with these variables:
- `AWS_ACCOUNT_ID`, `AWS_REGION`, `CI_ROLE_NAME` (OIDC role to assume)

and commit the matching `var/<env>/` files. The workflow assumes an IAM role via GitHub
OIDC — no long-lived keys.
OIDC — no long-lived keys.
2 changes: 2 additions & 0 deletions docs/logging.md → docs/en/logging.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Logging

[Français](../fr/logging.md) · **English**

## Architecture

SPIP logs → `logs_stderr` plugin → PHP `error_log()` → stderr → CloudWatch Logs
Expand Down
6 changes: 4 additions & 2 deletions docs/plugins.md → docs/en/plugins.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Managing SPIP plugins on Lambda

[Français](../fr/plugins.md) · **English**

SPIP runs as an **immutable Docker image** here. Plugins are baked into the image at
build time — there is no runtime plugin installation. Adding a plugin therefore means:
drop it in the repo, add a COPY in the Dockerfile if needed, rebuild, redeploy.
Expand All @@ -17,7 +19,7 @@ spip/
```

SPIP core plugins (`plugins-dist/`) are **not** in this repo — they come from the SPIP
core fetched at build time (see `docs/spip-upgrade.md`). A few of them are removed in the
core fetched at build time (see `docs/en/spip-upgrade.md`). A few of them are removed in the
Dockerfile (`bigup`, `forum`, `statistiques`, …) because they don't fit a serverless/
read-mostly deployment.

Expand Down Expand Up @@ -92,7 +94,7 @@ fetched SPIP core).

3. Data-model migrations go in `<prefix>_administrations.php` (SPIP's native schema
versioning — `spip_<prefix>_metas`/`maj_tables`). They run on the first authenticated
admin visit, or via `spip/scripts/bootstrap-db.php` (see `docs/db-bootstrap.md`).
admin visit, or via `spip/scripts/bootstrap-db.php` (see `docs/en/db-bootstrap.md`).

4. Rebuild + deploy.

Expand Down
2 changes: 2 additions & 0 deletions docs/s3-storage.md → docs/en/s3-storage.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# S3 File Storage

[Français](../fr/s3-storage.md) · **English**

## Problem

SPIP stores uploaded files (images, documents) in `IMG/` on the local filesystem. On Lambda, the filesystem is read-only (except `/tmp` which is ephemeral). Files must be stored externally.
Expand Down
2 changes: 2 additions & 0 deletions docs/secrets.md → docs/en/secrets.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Secrets Management (SSM Parameter Store)

[Français](../fr/secrets.md) · **English**

## Architecture

```
Expand Down
2 changes: 2 additions & 0 deletions docs/sessions.md → docs/en/sessions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Sessions (DynamoDB)

[Français](../fr/sessions.md) · **English**

## Problem

SPIP stores sessions as PHP files in `tmp/sessions/`. On Lambda, `/tmp` is ephemeral and per-instance — sessions are lost on cold starts and not shared across instances.
Expand Down
106 changes: 106 additions & 0 deletions docs/en/spip-passwords.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Resetting a SPIP password (SPIP-serverless / DSQL / Lambda)

[Français](../fr/spip-passwords.md) · **English**

Procedure to reset a SPIP author's password directly in the database,
when access to the private area is lost.

## Technical context

- SPIP runs in **Lambda** (Docker image), database **Aurora DSQL** (PostgreSQL).
- Table prefix: `spip` (SPIP default, configurable via SPIP_TABLE_PREFIX). The authors table is therefore `spip_auteurs`.
- Modern SPIP 4.x does **not** store a plain bcrypt of the password: the hash is
**peppered** with the site's `secret_des_auth` before bcrypt.

## Hashing formula (SPIP 4.x — `Spip\Chiffrer\Password::hacher`)

```php
$pass_poivre = hash_hmac('sha256', $password_clair, $key); // $key = secret_des_auth (BINAIRE)
$hash = password_hash($pass_poivre, PASSWORD_DEFAULT); // bcrypt ($2y$…)
```

Verification at login (`Password::verifier`) does:

```php
$pass_poivre = hash_hmac('sha256', $password_clair, $key);
password_verify($pass_poivre, $hash_stocke);
```

**Pitfall #1** — `Password::verifier` fails if `$secret` is empty. The secret comes from
the `cles.php` file (populated in the Lambda from the `SPIP_CLES` env var).

**Pitfall #2 (the most important)** — `SpipCles` **base64-decodes** the keys read from
`cles.php` (`array_map('base64_decode', $json)` in `SpipCles::read()`, and
`base64_decode()` in `getKey`/`getMetaKey`). The HMAC key is therefore the **decoded
binary** secret, NOT the base64 string as it appears in SSM/the JSON.

## Procedure

### 1. Retrieve the `secret_des_auth`

The Lambda's `SPIP_CLES` env var points to an SSM parameter:

```bash
AWS_PROFILE=<your-profile> aws lambda get-function-configuration \
--function-name spip-serverless-test-web --region us-east-1 \
--query "Environment.Variables.SPIP_CLES" --output text
# -> bref-ssm:/spip-serverless/test/spip/cles

AWS_PROFILE=<your-profile> aws ssm get-parameter \
--name "/spip-serverless/test/spip/cles" --with-decryption --region us-east-1 \
--query "Parameter.Value" --output text
# -> {"secret_du_site": "...", "secret_des_auth": "<secret_des_auth-base64>"}
```

### 2. Generate the hash (base64-DECODED secret)

```bash
SECRET_B64='<secret_des_auth-base64>' # secret_des_auth du SSM
NEWPASS='ChangeMe2026!'

HASH=$(php -r '
$secret = base64_decode($argv[1]); // IMPORTANT : décoder le base64
$poivre = hash_hmac("sha256", $argv[2], $secret);
echo password_hash($poivre, PASSWORD_DEFAULT);
' "$SECRET_B64" "$NEWPASS")

# Vérifier localement AVANT d'écrire en base
php -r '
$secret = base64_decode($argv[1]);
$poivre = hash_hmac("sha256", $argv[3], $secret);
echo password_verify($poivre, $argv[2]) ? "VERIFY OK\n" : "VERIFY FAIL\n";
' "$SECRET_B64" "$HASH" "$NEWPASS"
```

### 3. Write to the database (DSQL)

```bash
TOKEN=$(AWS_PROFILE=<your-profile> aws dsql generate-db-connect-admin-auth-token \
--hostname <cluster>.dsql.us-east-1.on.aws --region us-east-1)

PGPASSWORD="$TOKEN" psql \
"host=<cluster>.dsql.us-east-1.on.aws port=5432 dbname=postgres user=admin sslmode=require" \
-c "UPDATE spip_auteurs SET pass = '$HASH' WHERE id_auteur = 1 RETURNING id_auteur, login, email"
```

The current admin author: `id_auteur=1`, login `admin`, email `admin@example.com`,
status `0minirezo`, `webmestre=oui`.

## Notes / troubleshooting

- Login can be done with the **login** (`admin`) or the **email**.
- Do NOT touch `alea_actuel` (used to verify old sha256/md5 hashes, with no
impact on modern hashes).
- The repo's `src/…` file must match the SPIP version baked into the Lambda
image; check it in the image if needed:
`docker run --rm --entrypoint sh spip-serverless:latest -c "grep -n hash_hmac /var/task/ecrire/src/Chiffrer/Password.php"`
- **Changing the password from the private area**: may fail in a Lambda
environment (writing the `cles.php` file / backing up the keys on the read-only
`/var/task` filesystem, only `/tmp` is writable). The in-database reset above remains
the reliable method.

## Relevant SPIP code paths

- `ecrire/auth/spip.php` — `auth_spip_verifier_pass()` (dispatch based on `strlen($pass)`)
- `ecrire/src/Chiffrer/Password.php` — `hacher()` / `verifier()`
- `ecrire/src/Chiffrer/SpipCles.php` — `getSecretAuth()`, `getKey()`, `read()` (base64_decode)
Loading
Loading