Installation qualification protocol
| Document id | CS-VAL-06 |
| Title | Installation qualification (IQ) protocol |
| Version | 1.0 |
| Applies to | Countersign 1.0.0 |
| Status | Template supplied with the software; to be completed, reviewed and approved by the regulated user |
Approval of the completed document:
| Role | Name | Signature | Date |
|---|---|---|---|
| Prepared by | |||
| Reviewed by | |||
| Approved by (quality assurance) |
1. Purpose and scope
This protocol documents that Countersign is installed and configured as specified in one environment. Execute one copy per environment (test, production). It covers the server path with PostgreSQL and OpenID Connect sign-in, which is the recommended production arrangement; where you use SQLite or a reverse proxy, the alternative steps are marked.
The authoritative installation instructions are Deployment, Storage and Authentication. This protocol checks the result; it does not replace them.
| Item | Entry |
|---|---|
| Environment (test or production) | |
| Application address (public URL) | |
| Application server host name(s) | |
| Database server host name and database name | |
| Executed by (name, function) | |
| Witnessed or reviewed by (name, function) | |
| Start date |
2. Rules for execution
- Record the actual result in words or attach a numbered printout or screenshot. "As expected" is acceptable only where the expected result is a fixed text.
- Record values that identify what was installed (versions, hashes). Never record secrets: for a secret, record only "set" and where it is kept.
- Mark each step P (pass) or F (fail), with initials and date. A failed step needs a deviation (validation plan, section 8).
- Commands are written for the standalone repository.
countersignstands fornpm run cli --(ornode scripts/countersign.ts), run in the application directory with the sameCOUNTERSIGN_*environment as the server.
3. Prerequisites
| No. | Prerequisite | Evidence or reference | P/F | Initials, date |
|---|---|---|---|---|
| 3.1 | Validation plan, user requirements and risk assessment are approved | |||
| 3.2 | The configuration document is approved; its SHA-256 from countersign config validate <file> is recorded in the approval |
|||
| 3.3 | Server, operating system and network are qualified or under your infrastructure procedure | |||
| 3.4 | PostgreSQL 12 or newer is installed and operated by the database team | |||
| 3.5 | The application is registered in the identity provider (redirect address <public URL>/auth/callback), roles or groups exist |
|||
| 3.6 | The release to install is identified (tag and commit) and was obtained from the project's repository | |||
| 3.7 | The person executing is trained and has the necessary access; the database owner credentials are with the database administrator only |
4. Versions and identification
| No. | Instruction | Expected result | Actual result | P/F | Initials, date |
|---|---|---|---|---|---|
| 4.1 | On each application server run node --version |
22.18 or newer | |||
| 4.2 | On the database run SELECT version(); |
PostgreSQL 12 or newer | |||
| 4.3 | In the application directory run git describe --tags --always and git rev-parse HEAD (or record the checksum of the release archive) |
The approved release: tag ______, commit ______ | |||
| 4.4 | Run git status --porcelain (if installed from the repository) |
No output: no local modification of the source | |||
| 4.5 | Compute the checksum of the schema file: sha256sum db/postgres.sql |
Recorded; equal on every server and equal to the file in the approved release | |||
| 4.6 | Record the installed dependency tree: npm ls --all > npm-ls.txt and npm sbom --sbom-format cyclonedx > sbom.json; attach both |
Files produced without errors about missing or extraneous packages | |||
| 4.7 | Run npm audit --omit=dev and attach the output |
No known vulnerability rated high or critical, or each one assessed in a deviation | |||
| 4.8 | Record operating system and version of each server | Recorded | |||
| 4.9 | Record the reverse proxy or load balancer product and version, if any | Recorded |
5. Environment variables
Record the value for non-secret variables; for secrets record "set" and the secret store. See the functional specification, section 9.1, for meanings.
| No. | Variable | Required value or rule | Actual (value, or "set") | P/F | Initials, date |
|---|---|---|---|---|---|
| 5.1 | COUNTERSIGN_STORAGE |
postgres (or the adapter named in the validation plan; never memory, never unset) |
|||
| 5.2 | COUNTERSIGN_DATABASE_URL |
Secret. Set; names the application role, not the owner or a superuser; TLS to the database as your policy requires | |||
| 5.3 | COUNTERSIGN_SKIP_MIGRATE |
1 when the application role has no DDL rights (recommended, see section 6) |
|||
| 5.4 | COUNTERSIGN_AUTH |
entra or oidc (or proxy if approved in the plan); never demo |
|||
| 5.5 | COUNTERSIGN_ENTRA_TENANT_ID, COUNTERSIGN_ENTRA_CLIENT_ID (entra) or COUNTERSIGN_OIDC_ISSUER, COUNTERSIGN_OIDC_CLIENT_ID (oidc) |
Set to the registered application | |||
| 5.6 | COUNTERSIGN_ENTRA_CLIENT_SECRET or COUNTERSIGN_OIDC_CLIENT_SECRET |
Secret. Set (required for entra); expiry date of the secret recorded in your calendar | |||
| 5.7 | COUNTERSIGN_ID_CLAIM |
Unset (default oid for entra, sub for oidc) or the provider's immutable unique identifier |
|||
| 5.8 | COUNTERSIGN_NAME_CLAIM |
Unset (default name) or a claim holding the person's full name |
|||
| 5.9 | COUNTERSIGN_ROLE_CLAIM |
Unset (default roles for entra, groups for oidc) or the claim your provider uses |
|||
| 5.10 | COUNTERSIGN_ROLE_MAP |
The approved map, as JSON; attach it. Every target is a role id of the configuration | |||
| 5.11 | COUNTERSIGN_SESSION_SECRET |
Secret. Set; at least 32 characters, randomly generated; identical on all servers | |||
| 5.12 | COUNTERSIGN_SESSION_HOURS |
The value in your plan (default 8) | |||
| 5.13 | COUNTERSIGN_SIGN_MAX_AGE_MINUTES |
The value in your plan (default 15); not 0 |
|||
| 5.14 | COUNTERSIGN_PUBLIC_URL |
The https address people use, without a trailing slash | |||
| 5.15 | COUNTERSIGN_CONFIG |
Unset, or the path of the approved configuration file (it is applied only to an empty store) | |||
| 5.16 | COUNTERSIGN_DEMO_DATA |
Unset | |||
| 5.17 | COUNTERSIGN_PROXY_SECRET and the three COUNTERSIGN_PROXY_*_HEADER variables (proxy sign-in only) |
Secret set, at least 16 characters; header names as the proxy sends them | |||
| 5.18 | Where the variables are defined (service unit, container definition, secret store) and who can read them | Recorded; readable only by the service account and administrators |
6. Database
Recommended arrangement: an owner role that owns the table, functions, triggers and view and is used only by the database administrator; an application role that can only read and append. The authoritative commands are in Storage; in summary, with example role names:
-- As a database administrator, once (passwords set separately, never recorded here):
CREATE ROLE countersign_owner LOGIN;
CREATE ROLE countersign_app LOGIN;
CREATE DATABASE countersign OWNER countersign_owner;
GRANT CONNECT ON DATABASE countersign TO countersign_app;
-- As countersign_owner, in that database, apply the reviewed schema:
-- psql "<owner connection>" -v ON_ERROR_STOP=1 -f db/postgres.sql
GRANT USAGE ON SCHEMA public TO countersign_app;
GRANT SELECT, INSERT ON countersign_entries TO countersign_app;
GRANT SELECT ON countersign_audit_trail TO countersign_app;
With this arrangement start the server and the command-line tool with COUNTERSIGN_SKIP_MIGRATE=1; otherwise they try
to apply the schema themselves on start and fail for lack of rights.
| No. | Instruction | Expected result | Actual result | P/F | Initials, date |
|---|---|---|---|---|---|
| 6.1 | Confirm the table: SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'countersign_entries' ORDER BY ordinal_position; |
Ten rows: seq bigint, at timestamp with time zone, actor text, action text, record_id text, record_type text, prev_hash character, hash character, entry text, inserted_at timestamp with time zone |
|||
| 6.2 | Confirm the triggers in the catalogue: SELECT tgname, tgenabled, pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'countersign_entries'::regclass AND NOT tgisinternal ORDER BY tgname; |
Three rows, each with tgenabled = O: countersign_entries_chain (BEFORE INSERT, FOR EACH ROW, function countersign_entries_chain), countersign_entries_no_change (BEFORE DELETE OR UPDATE, FOR EACH ROW, function countersign_entries_append_only), countersign_entries_no_truncate (BEFORE TRUNCATE, FOR EACH STATEMENT, function countersign_entries_append_only) |
|||
| 6.3 | Cross-check in the information schema: SELECT trigger_name, event_manipulation, action_timing, action_orientation FROM information_schema.triggers WHERE event_object_table = 'countersign_entries' ORDER BY 1, 2; |
Three rows: countersign_entries_chain INSERT BEFORE ROW; countersign_entries_no_change DELETE BEFORE ROW; countersign_entries_no_change UPDATE BEFORE ROW. (The TRUNCATE trigger is not listed in this view; step 6.2 covers it) |
|||
| 6.4 | Confirm the trigger functions match the reviewed file: SELECT proname, md5(prosrc) FROM pg_proc WHERE proname IN ('countersign_entries_chain', 'countersign_entries_append_only'); and compare prosrc with db/postgres.sql by reading |
Two rows; the function bodies are those of the file | |||
| 6.5 | Confirm constraints: SELECT conname, contype, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'countersign_entries'::regclass; |
A primary key on seq, a unique constraint on hash, a check seq > 0 |
|||
| 6.6 | Confirm the view: SELECT viewname, viewowner FROM pg_views WHERE viewname = 'countersign_audit_trail'; and read its definition with SELECT pg_get_viewdef('countersign_audit_trail'::regclass, true); |
One row, owned by the owner role; every column except inserted_at is taken from entry (as in db/postgres.sql) |
|||
| 6.7 | Confirm ownership: SELECT tableowner FROM pg_tables WHERE tablename = 'countersign_entries'; |
The owner role, not the application role | |||
| 6.8 | Confirm the application role's attributes: SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolreplication, rolbypassrls FROM pg_roles WHERE rolname = 'countersign_app'; |
All five flags false | |||
| 6.9 | Confirm the application role's table privileges: SELECT privilege_type FROM information_schema.table_privileges WHERE table_name = 'countersign_entries' AND grantee = 'countersign_app' ORDER BY 1; |
Exactly two rows: INSERT, SELECT | |||
| 6.10 | Confirm what it cannot do: SELECT p, has_table_privilege('countersign_app', 'countersign_entries', p) FROM unnest(ARRAY['UPDATE','DELETE','TRUNCATE','TRIGGER','REFERENCES']) AS p; |
Five rows, all false | |||
| 6.11 | Confirm it is not a member of a privileged role: SELECT r.rolname FROM pg_auth_members m JOIN pg_roles r ON r.oid = m.roleid WHERE m.member = 'countersign_app'::regrole; |
No row (or only roles without rights on the table; list them) | |||
| 6.12 | List every role with more than SELECT and INSERT on the table: SELECT grantee, privilege_type FROM information_schema.table_privileges WHERE table_name = 'countersign_entries' AND privilege_type NOT IN ('SELECT','INSERT'); and list superusers: SELECT rolname FROM pg_roles WHERE rolsuper; |
Only the owner role and the named administrators; record the names | |||
| 6.13 | Confirm durability settings: SHOW fsync; SHOW synchronous_commit; SHOW full_page_writes; |
on, on (or stricter), on |
|||
| 6.14 | Confirm the database server's time zone handling and clock: SELECT now(), current_setting('TimeZone'); compared with a reference clock |
Within your tolerance (for example 1 second) of the reference | |||
| 6.15 | Confirm database audit logging of DDL and role changes, if your policy requires it: SHOW log_statement; or the audit extension's settings |
ddl or stricter, or the extension is configured |
SQLite instead of PostgreSQL: replace 6.1 to 6.12 by sqlite3 <file> "SELECT name, sql FROM sqlite_master WHERE type = 'trigger';" (expected: countersign_entries_chain, countersign_entries_no_update, countersign_entries_no_delete)
and by a record of the file's owner and permissions (readable and writable by the service account only).
7. Initialisation and application
| No. | Instruction | Expected result | Actual result | P/F | Initials, date |
|---|---|---|---|---|---|
| 7.1 | On a new installation: countersign init --config <approved configuration file> |
"Initialised the postgres store with 1 entry from <file>. Head #1 <hash>" (on an existing installation: "The store already holds <n> entries; nothing to do.") |
|||
| 7.2 | countersign status |
Lines store postgres, entries <n>, head hash <64 hex>, configuration <id> "<name>", revision <r>, SHA-256 <64 hex>, records …, integrity ok; exit code 0 |
|||
| 7.3 | Compare the configuration SHA-256 from 7.2 with the one in the configuration's approval (3.2) | Identical | |||
| 7.4 | countersign verify |
hash chain intact, replay all <n> entries reproduce, head hash <same as 7.2>; exit code 0 |
|||
| 7.5 | Start the application service on every server; check the service log | Started without error | |||
| 7.6 | From the monitoring host: curl -s -o health.json -w '%{http_code}\n' <public URL>/api/health and attach health.json |
HTTP 200; "status":"ok", "mode":"server", "version":"1.0.0", "storage":"postgres", "auth": the approved method, "entries" and "headHash" equal to 7.2, "config" with the id, revision and hash of 7.2, "fault":null |
|||
| 7.7 | Repeat 7.6 against each application server directly, if there are several | Same headHash on all |
|||
| 7.8 | Negative check of the settings guard, test environment only: start one instance with COUNTERSIGN_SESSION_SECRET unset and request /api/health |
HTTP 503; "status":"misconfigured"; problems contains "COUNTERSIGN_SESSION_SECRET must be at least 32 characters (for example: openssl rand -base64 48)." Restore the setting afterwards |
8. Network, TLS and time
| No. | Instruction | Expected result | Actual result | P/F | Initials, date |
|---|---|---|---|---|---|
| 8.1 | Open <public URL>/app in a browser and inspect the certificate |
Valid certificate for the host name, issued by a trusted authority, not expired; protocol TLS 1.2 or newer | |||
| 8.2 | Request the http address: curl -sI http://<host>/app |
Redirect to https, or connection refused | |||
| 8.3 | After signing in (section 9), inspect the cookie countersign_session in the browser's developer tools |
Flags HttpOnly, Secure, SameSite=Lax |
|||
| 8.4 | Confirm that the application port is reachable only from the reverse proxy or load balancer (firewall rule or a connection attempt from a workstation) | Not reachable directly | |||
| 8.5 | Confirm that the database port is reachable only from the application servers and administration hosts | As specified | |||
| 8.6 | On every application server show time synchronisation: timedatectl status or chronyc tracking |
Synchronised to the organisation's time source; offset within tolerance | |||
| 8.7 | Confirm monitoring of time synchronisation | Alert defined | |||
| 8.8 | Proxy sign-in only: send a request to the application through the proxy with a forged X-Forwarded-User header and with a forged X-Countersign-Proxy-Secret header from a client |
The proxy removes or overwrites both; the application shows the real identity or answers 401 |
9. Sign-in and roles
Use real accounts of the identity provider. Record account names, never passwords.
| No. | Instruction | Expected result | Actual result | P/F | Initials, date |
|---|---|---|---|---|---|
| 9.1 | Without a session: curl -s -o /dev/null -w '%{http_code}\n' <public URL>/api/entries |
401 | |||
| 9.2 | Open <public URL>/app in a fresh browser session and sign in as test user 1 |
Redirect to the identity provider, sign-in with the provider's rules (including multi-factor if required), return to the application; the application shows the user's full name and role(s) | |||
| 9.3 | Request <public URL>/api/bootstrap in the signed-in browser |
"user" shows the expected id (the provider's immutable identifier), name and roles (role ids of the configuration); "auth" is the approved method; "signMaxAgeMinutes" is the planned value |
|||
| 9.4 | Decode the ID token or consult the provider's token configuration | The token contains the role claim and auth_time. If auth_time is absent, raise a deviation: the application then uses the token's issue time for the recent-sign-in rule (risk assessment, R-12) |
|||
| 9.5 | Sign in as test user 2 with a different role | The expected role(s) | |||
| 9.6 | Sign in as a user who is assigned to the application but holds no mapped role | Signed in; can read; every write action is unavailable | |||
| 9.7 | Try to sign in as a user who is not assigned to the application in the provider | Refused by the provider (if assignment is required in your design) | |||
| 9.8 | Review the role map against the configuration: every role id of the configuration that is used has at least one provider group; no provider value that could reach the application equals a role id unintentionally | Confirmed; attach the list of groups and members at the time of execution | |||
| 9.9 | Sign out | The application returns to its signed-out state; /api/entries answers 401 in that browser |
|||
| 9.10 | Confirm segregation: no database or server administrator holds an author or approve role | Confirmed against the lists of 6.12 and 9.8 |
10. Backup, monitoring and documents
| No. | Instruction | Expected result | Actual result | P/F | Initials, date |
|---|---|---|---|---|---|
| 10.1 | Show the backup configuration of the database (schedule, retention, location, point-in-time recovery) | As specified in the backup SOP | |||
| 10.2 | Show the last successful backup | Present and within schedule | |||
| 10.3 | Restore the latest backup into a scratch database and run countersign verify against it (environment variables pointing at the scratch database) |
hash chain intact, replay all <n> entries reproduce; head hash equals that of the source at the time of the backup |
|||
| 10.4 | Show the monitoring check on /api/health and its alert |
Alerts on any HTTP status other than 200 and on status other than ok; recipients named |
|||
| 10.5 | Show the schedule for countersign verify (for example a daily job whose non-zero exit code raises an alert) |
Defined | |||
| 10.6 | Record the first head-hash anchor: number of entries and head hash from 7.2, written to the anchor register outside the database administrators' reach | Anchor recorded; reference: ______ | |||
| 10.7 | Confirm that the SOPs listed in the validation plan are effective and that operators are trained | Confirmed | |||
| 10.8 | Confirm that web-server access logs are kept for the period in your policy and include the status code of requests to /api/act |
Confirmed |
11. Deviations
| No. | Step | Description | Classification | Resolution | Closed (initials, date) |
|---|---|---|---|---|---|
12. Attachments
| No. | Attachment |
|---|---|
| 1 | npm-ls.txt, sbom.json, npm audit output |
| 2 | Output of the SQL queries of section 6 |
| 3 | health.json, output of countersign status and countersign verify |
| 4 | Role map, list of provider groups and members |
| 5 | Screenshots of sign-in and cookie flags |
13. Conclusion
| Statement | Yes / No |
|---|---|
| All steps passed or deviations are closed | |
| The installation is released for operational qualification (test) / for use (production, together with the summary report) |
| Role | Name | Signature | Date |
|---|---|---|---|
| Executed by | |||
| Reviewed by (system owner) | |||
| Approved by (QA) |