Files

78 lines
11 KiB
Markdown

# Privacy and personal data
This page is for operators: whoever runs a RenewCMS instance is the data controller for it under the GDPR. It lists what the software stores about people, where, for how long, and what it sends elsewhere, so you can fill in your record of processing (art. 30), write your privacy notice (art. 13) and answer requests from the people concerned. It describes the software, not your obligations; it is not legal advice.
## Who is concerned
- **Back-office users**: the owner and the admins they invite.
- **Invitees** who have not accepted yet.
- **People named in content**: authors, and anyone your editors write about or photograph.
- **Anyone who performs an audited action** (logging in, failing to log in, changing anything) has their IP address and browser recorded in the activity log.
Readers of your public site are not tracked by RenewCMS: the public routes set no cookie and log nothing in production.
## What is stored, and for how long
| Data | Where | Kept |
|---|---|---|
| Username, email, display name, role, bcrypt password hash | `users` table | Until the user deletes the account (see Erasure under [Answering requests from the people concerned](#answering-requests-from-the-people-concerned)). |
| Verification and password-reset codes | `users` table, bcrypt-hashed | Verification: until the account is verified. Reset: one hour, then useless; cleared when used. |
| Pending email change: the new address (`pending_email`), its bcrypt-hashed code (`email_change_code`) and the expiry (`email_change_expiration`) | `users` table | Two hours, then useless; cleared when confirmed, cancelled or replaced. |
| Unverified sign-ups | `users` table | Deleted automatically 7 days after their verification code expired. |
| Invites: invitee email, hashed code, inviter | `invites` table | Deleted automatically 30 days after they expired or were accepted. |
| Sessions | The `jwt` cookie in the user's browser, valid 24 hours | Logging out revokes the session on the server; the revocation (a random id and an expiry, nothing else) is deleted once the session would have expired anyway. |
| API keys: name, prefix, SHA-256 hash, creator, last use | `api_keys` table | Until revoked. A revoked key stays listed. |
| Activity log: who (user id and name, API key name, or the username typed at a failed login), what, when, IP address, browser (user agent, truncated to 255 characters) | `audit_events` table | Settings → Activity: 30, 90 (default), 365 days or forever, and an optional size cap. Cleaned up once a night, at 03:00 server time. |
| Authors: name, slug, bio, avatar, website, social links; the linked user's email is shown to admins | `authors` table | Until deleted. When the linked user deletes their account, the author is deleted if no article credits it, otherwise it is kept as "Deleted user" with no bio, avatar, website or links. |
| Articles and custom entries | `articles`, `entries` tables | Deleted articles are soft-deleted: the row and its content stay in the database (see [Gaps](#gaps)). |
| Uploaded images | `UPLOAD_DIR` or the S3 bucket, plus the `images` table (file name, size, uploader) | Until deleted from the media library. Deleted images are soft-deleted in the table; the file itself is removed. |
### Photo metadata
Since this version, uploads are cleaned before they are stored. JPEG, PNG and WebP files lose their EXIF (GPS position, camera serial number, date), XMP, IPTC, comments and text chunks; only the orientation is kept, so rotated photos still display correctly. The pixels are not re-encoded. SVG files are stored as uploaded. **Images uploaded with an earlier version are not cleaned retroactively**: if they may carry GPS positions, download, clean and re-upload them, or merge them into a cleaned copy from the media library.
### Activity log retention
For security logs, the CNIL generally recommends keeping access logs between six months and a year. The default of 90 days is shorter; choose 365 days in Settings if that suits your purpose. Avoid "Forever": it keeps IP addresses indefinitely, which the storage-limitation principle rarely justifies.
## Cookies and browser storage
- `jwt`: the session cookie. HttpOnly, `SameSite=Lax`, `Secure` in production, 24 hours. Strictly necessary for the back office, so it needs no consent (ePrivacy art. 5(3); in France, Loi Informatique et Libertés art. 82).
- `localStorage` on the user's device holds interface choices only: language (`renewcms-language`), page sizes, open or collapsed panels, dismissed hints and columns. `sessionStorage` holds unsaved article drafts until the tab closes. None of it is sent to the server or to anyone else, and it needs no consent either.
RenewCMS sets no analytics, advertising or third-party cookie, so no consent banner is needed for the back office. Your public site may need one if *it* adds trackers.
## What leaves the server
| Destination | When | What |
|---|---|---|
| Your SMTP provider (`SMTP_HOST`) | Sign-up, invite, password reset, email change, account deleted | Recipient address, the message (with a one-time link, except the account-deleted notice, which holds the site address only). STARTTLS or implicit TLS is required for any non-loopback host unless you set `SMTP_ALLOW_INSECURE=true`. |
| Your S3 provider, if `STORAGE_DRIVER=s3` | Every upload and image read | The image files. |
| `UPDATE_FEED_URL` (default `https://git.floriansylvain.fr`) | Only when the owner clicks "Check for updates" or downloads one | The server's IP address and a standard HTTP request. No account or content data. |
The back office loads nothing from third parties: fonts, icons and scripts are served by RenewCMS itself, and the Content-Security-Policy forbids any other source. Pick EU-based SMTP and storage providers, or make sure transfer safeguards (art. 44 and following) cover them, and list them as processors.
## Security measures you can cite (art. 32)
- Passwords are hashed with bcrypt (cost 12). One-time codes are random UUIDs, stored hashed, single-use and short-lived. API keys are 256-bit random values stored as SHA-256 hashes.
- Login, password-reset and invite routes are rate-limited per client IP; failed logins are recorded as security events.
- Sessions are revocable: logout ends one session, a password reset ends all of them and revokes the user's API keys.
- Cross-site writes are refused, cookies are `SameSite=Lax`, and a strict Content-Security-Policy, `X-Frame-Options: DENY` and HSTS (in production) are sent.
- The server refuses to start with a short or example `JWT_SECRET`.
- Release binaries are signed (ed25519) and checked before a browser update is installed.
## Answering requests from the people concerned
- **Access and portability (art. 15 and 20)**: every verified user downloads a copy of their data from the Your data card on Profile (`GET /v1/user/me/export`). It is a ZIP of four pretty-printed JSON files: `profile.json` (account, role, author profile, API-key metadata without the key or its hash), `authored-content.json` (every article they created, drafts included, the published articles crediting their author profile, and entries they created; credited authors appear as id, name and slug only; other people's drafts are never included), `uploaded-media.json` (references to the files they uploaded, no bytes) and `account-activity.json` (the events where they are the actor). It never holds the password hash, any code, the token version or other people's data, and it leaves out anonymous failed-login events (their actor is not an account), events whose target is another user, an invite or another person's author profile, and soft-deleted articles and images. The archive is built in memory, and at most 2 exports are built at the same time across all users; a request whose wait for a free slot is cancelled or times out answers 503 and does not use the user's slot. One export is allowed per user every 15 minutes, and each one is recorded as `user.export_data`. An owner can also export the activity log as CSV from the Activity page and filter it by person.
- **Rectification (art. 16)**: users can change their display name, author profile and email address themselves. An email change needs the current password and a link sent to the new address; the old address is told about the request and keeps working until the link is opened.
- **Erasure (art. 17)**: every verified user deletes their own account from the Danger zone card on Profile (`DELETE /v1/user/me`), with their password and the typed word `DELETE`; an unverified account can still cancel its own registration. The last Owner, and the last administrator (built-in Admin role with no Owner), are refused until another verified user holds the role, so the workspace is never left without one. In one atomic operation, **removed**: the user row (username, display name, email, password hash, codes), every API key, every session, and the linked author profile when no article credits it. An author profile credited on articles is **scrubbed**, not deleted: it becomes "Deleted user" with the slug `deleted-user-<id>` (that prefix is reserved, so nobody can pick it), no bio, website, social links or user link. Its **avatar image** is deleted (the media row in the same operation, the stored file and its generated variants right after the commit; a storage failure is logged and does not undo the deletion) unless the image is also used by an article (body, cover or social image), another author, a site's default social image or a custom entry, in which case it stays. In the **activity log** the events where the user is the actor or the target lose their name (it becomes "Deleted user"), their IP address and browser, and their `params` and `changes` (which can hold display names, author names, bios and invitee emails); author events the user made on their own profile also lose the profile name. The events themselves (action, outcome, time) stay, and so do author events another person made on that profile, and events written by other people about the user, which keep their text but not the user's name. A mail "Your RenewCMS account has been deleted" (the site address only) is sent to the address the account had, and the deletion is recorded as a `user.delete_account` event under "Deleted user", with the IP address and browser of the request: it is the one trace of the erasure.
What remains, honestly: articles, entries and media the user created keep the old user id in `created_by` / `updated_by` / `uploaded_by_id` (shown as "Deleted user"; the id no longer points to anyone); `invites.invited_by` can point at the deleted id; anonymous failed-login events keep the username that was typed (see [Gaps](#gaps)); soft-deleted articles and images stay as rows (see [Gaps](#gaps)); and the browser of the person may still hold unsaved drafts and interface choices in `localStorage` / `sessionStorage` on their device, which RenewCMS cannot reach.
## Gaps
These are known and tracked; until they are fixed, handle them by hand.
- Soft-deleted articles and images keep their rows indefinitely.
- The username typed at a failed login is stored as typed, so a password typed in the wrong field can end up in the activity log; clean such events by hand if it happens.
- Images uploaded before metadata stripping keep their metadata.