← Back to directory

security-and-operations.md

implementation/security-and-operations.md · 38.4 KB · 2026-11-02 12:48

Security and Operations

Scope and Assurance

Status: retained source evidence and chosen initial operational specification, with bounded runtime evidence current to 2026-09-30. Final D01-D10 are settled; operational controls are only partially implemented and the platform is not security-certified. See implementation status for the scope actually verified. This document translates identity and governance into implementation and operational checks; it does not define identity or policy semantics. Current source observations and focused component tests are not a security certification, penetration test, deployment verification or full new-platform acceptance. Implementation architecture specifies the initial runtime and database mechanisms; identity and governance defines authority. Hosting configuration must satisfy those contracts, not reopen them. The old organization/client/admin hierarchy is not the new authorization model. Preserve isolation and explicit authority, not those tables or roles. Documentation workflow and lineage belong to documentation governance and the source map.

Current Configuration Contract

This section records the former runtime's configuration as historical evidence only. New-runtime mechanisms below must be implemented and checked before operational use.

The removed legacy configuration preferred a nonempty process environment value, then a string value from an external PHP array, then the caller's default. The external array is cached for that process/request context. The application does not itself load dotenv files; no environment-example file is promised here. Do not inspect or print live secrets to verify this contract; inspect source and use isolated nonsecret fixtures.

| Name | Observed purpose and default | | --- | --- | | APP_CONFIG_FILE | Overrides the external secrets file path through the process environment | | DB_DSN | Optional explicit PDO DSN; must start with mysql: in the old runtime | | DB_HOST | Host when constructing the DSN; no application host default | | DB_PORT | Optional port segment; omission leaves port selection to the driver, not a hard-coded application value | | DB_NAME | Database name when constructing the DSN | | DB_USER, DB_PASS | Connection credentials, also used with an explicit DSN | | DB_CHARSET | Defaults to utf8mb4 when constructing the DSN | | APP_ENCRYPTION_KEY | 64 hexadecimal characters representing 32 key bytes for the current crypto helper | | APP_ENCRYPTION_KEY_VERSION | WordPress credential metadata label, default v1; not implemented multi-key decryption | | GROQ_API_KEY | Optional credential enabling the existing WordPress generation request | | GROQ_MODEL | Defaults to llama-3.3-70b-versatile | | GROQ_API_URL | Defaults to https://api.groq.com/openai/v1/chat/completions |

The removed legacy database helper constructed mysql:host=<host>;port=<port>];dbname=<name>;charset=<charset> when DB_DSN was absent. An explicit DSN supplies its own charset/port; the separate variables are not appended to it. Current PDO options are exception errors, associative fetches and native prepares; the target uses PDO with the pdo_mysql driver against MariaDB 10.5.26/InnoDB. The mysql: PDO DSN and driver name are the expected PDO mechanism for MariaDB; other database drivers are rejected. This is the initial production compatibility contract, not a claim of portability to other database engines. Without GROQ_API_KEY, generation is unavailable; manual building, local saves and remote draft creation have no AI-provider dependency, though their other prerequisites still apply. Groq is an optional old adapter detail, not a provider-specific core requirement; the [AI owner governs the no-AI guarantee.

External Secrets and Encryption

The observed default configuration path is one directory above the project root: .idiosyncrasy-secrets.php via dirname(__DIR__, 3) in Config. The documented Plesk example is /var/www/vhosts/idiosyncrasy.co.uk/.idiosyncrasy-secrets.php, outside the platform document root. This is a known deployment path, not confirmation that a file exists or was read, and not a required future host. Where PHP-FPM environment variables are unavailable, an operator can provide the external PHP array at an approved path and restrict access to the subscription owner/runtime identity. Because it is executed with PHP require, the secrets file must be trusted configuration, never user-uploaded content. Keep credentials, encryption keys, provider tokens, runtime uploads and deployment credential files out of source control and public deployment bundles.

Target Release and Local Execution Controls

The operator-only bin/reset-principal-password.php boundary is a staging recovery mechanism for an already-confirmed User (legacy Type name Principal) when no interactive password-reset channel is available. It requires --database=target, --confirm-target=staging, IDY_ALLOW_TARGET_PROVISIONING=1, the resolved staging database configuration and a password supplied through IDY_OPERATOR_PASSWORD; the password is never a command-line argument or release value. It validates the User's active status, updates the Argon2id credential through PrincipalCredentialResetService, and records a principal.password_reset security-audit event in the same transaction. It is not an HTTP route, does not bypass confirmation, and must not be used for production or by editing credential tables directly.

The operator-only bin/reconcile-foundation.php boundary is a staging-only forward reconciliation for the approved IDY-003 Business Type descriptor. It requires the same explicit target safeguards and authenticated User, appends the descriptor as object revision 2 through the generic repository, records foundation_reconciliations, and leaves the immutable original genesis_runs record untouched. It accepts only the repository's approved current manifest and the supported predecessor descriptor; it is idempotent and fails closed for any other foundation drift.

The reviewed target release definition is the authority for target composition. Release tooling resolves only its approved paths, rejects traversal, absolute paths and links, and emits deterministic SHA-256 manifest evidence. The current source boundary includes the persisted Definition validator, governed publication Operation handler, Capability Application handler and persisted projector, server-resolved Operation input reader, exact Operation-identity Action authorization, no-network send_email Provider/handler, core-object Presentation provisioning, shared configuration runtime-ID resolver and holistic view, immutable runtime vocabulary descriptors, definition-driven initialization classes, target wordmarks and User confirmation-delivery classes (legacy class names use Principal); provisioning-only publisher installation, Genesis and platform-opening materials remain outside the public runtime package. The retired historical staging snapshot is not part of current release evidence. A package is eligible for deployment only when its canonical manifest carries GitHub authority, repository, exact full commit SHA and non-secret build identity. The internal StagingProvisioningPackage is a distinct operator-only artifact bounded by its own manifest and required provisioning paths. It includes required provisioning runtime classes, including the publisher authority installer, local runtime environment loader and no-network Communication handlers; it is not the public target release package and is not published by the GitHub deployment workflow. Current package-test and read-only target-check evidence is recorded in implementation status.

Protected CLI provisioning commands use the process environment as their authoritative runtime configuration. bin/provision-target.php now loads only an existing local .env.local file through the allowlisted LocalRuntimeEnvironment helper when a value is not already present in the process environment. It loads only the target database, staging database-name, target runtime and Definition-publisher setting names; it does not print, persist or override explicit process values. The file remains local operator configuration and is never packaged or used by the web runtime. On a STAGING operator workstation, the protected command can therefore use the existing local configuration without placing credentials in command-line arguments. The command still enforces the explicit staging database identity and all existing provisioning safeguards. After local package verification, one recursive SCP operation transfers the manifest-bound public package and two control files into the release-specific non-active .releases/<release-id>-staging path below the documented application root. The verified public release is installed before the existing /health check. bin/staging-deploy.sh re-verifies the downloaded package, requires the manifest-bound package identity and target/.htaccess, uses sshpass -d to provide the staging password to OpenSSH and SCP without placing it in command arguments, and emits only non-secret repository, commit, release, digest and URL evidence.

bin/target-check.ps1 is read-only and does not connect to a database. bin/target-local.ps1 requires the pinned PHP runtime, reads the local connection file outside the repository, accepts loopback hosts only, rejects production-looking database names, clears legacy database variables and uses var/target-sessions-local. These controls are local-development safeguards, not production deployment or hosting certification. Production activation, credential provisioning, migrations, backups, ingress checks and rollback remain unverified operational work.

bin/target-release.ps1 reports local preflight, GitHub-bound package verification, production connection, staging/upload, remote verification, blocked activation and final result as explicit operator-visible phases. It consumes an existing package and never silently builds from the local working tree. It reads only the existing .deploy/idiosyncrasy-production.json metadata and an external SSH identity path. It supports non-interactive OpenSSH key authentication and an operator-controlled native interactive password fallback (Auto, Key or Password mode); the password is never captured, stored, passed as a command-line argument, logged, or packaged. Strict host-key verification remains mandatory. After local package verification, one recursive SCP operation transfers the manifest-bound public package and two control files into the release-specific non-active .releases/<release-id>-staging path below the documented application root. Remote verification then compares the exact reviewed manifest paths, byte counts, SHA-256 values and GitHub provenance. Native SSH/SCP failures report the executable, non-secret operation context, exit code, captured output and PowerShell exception details when present, without printing command arguments or password material. Failed staging remains identifiable and the active tree and var/target-sessions are not removed or replaced.

bin/target-activate.ps1 -ReleaseId <release-id> -ArtifactDigest <digest> -CommitSha <sha> is the separate explicit activation operator for an already-uploaded release. It does not rebuild or upload. Its remote preflight now requires matching release ID/artifact digest plus GitHub repository and exact commit provenance before the existing filesystem switch could be attempted. The activation unit is the complete release root under .releases/<release-id>-staging; the fixed Plesk document root remains target, which is switched to a symlink targeting that release's target/ directory so relative ../bootstrap, app and resources resolution stays within the same release root. Before switching, the existing real target directory is moved intact into a release-specific previous-live rollback directory. A lock prevents concurrent activation; any pointer, identity or HTTP /register failure restores the previous target automatically. On success, the previous target remains retained for rollback. This operation performs no database, session, migration, provisioning, Genesis, platform-opening, environment-variable, Plesk or document-root configuration changes.

The ordinary release command remains non-activating and refuses migrations, provisioning, Genesis, platform opening, schema changes, data mutation, credential creation and Plesk configuration changes. target-activate.ps1 is the only activation path; it requires the release-specific staging prerequisite and a successful post-switch filesystem and HTTP /register check before reporting success. It is not a substitute for remote manifest verification.

The former bin/target-upload-direct.ps1 path has been removed. A push to main runs the build and provenance job, then the protected deploy-staging job invokes bin/staging-deploy.sh automatically for application package installation and health verification only. After that workflow completes successfully for a main push, the protected staging-presentation-profile-migration.yml workflow is invoked by workflow_run for the same commit and reconciles the version-controlled Presentation profile. A manual workflow_dispatch remains available for non-deploying build and package diagnostics and for an explicitly selected Presentation migration. The helper uses only STAGING_SSH_HOST, STAGING_SSH_PORT, STAGING_SSH_USER, STAGING_SSH_PASSWORD, STAGING_REMOTE_ROOT and STAGING_URL for transport, install and health verification. It receives no staging database credentials and does not run reset, migration, Genesis, platform opening or provisioning; those operations are internal-script-only and must be explicitly authorized outside GitHub Actions. The verified manifest-bound public release is installed before the existing /health check. The job does not use production credentials or a production database path. The push workflow builds and deploys the exact artifact from its triggering github.sha; it does not select an arbitrary latest artifact or consume an artifact from another run.

The automatic Presentation migration checks the completed upstream workflow conclusion, push event, main branch and same-repository identity before running. It checks out the upstream workflow's exact head_sha, not the migration workflow's default-branch tip. The migration continues to use the protected SSH route and the existing extend-business-presentation.php command. Its Presentation AuthorityGrant ID remains an explicit allowlisted target, while the command reads the current grant version immediately before any governed update and supplies that observed version as the optimistic expected version. This removes stale manually copied version inputs without weakening CommandHandler validation, authority, transaction or provenance checks.

Development Staging v1 deliberately uses password SSH with host-key verification disabled through StrictHostKeyChecking=no and UserKnownHostsFile=/dev/null, solely because the Development Staging v1 requirement is username/password authentication without a pre-populated host-key file. This staging-only transport is not the production deployment transport and must not be inherited by production operations.

bin/staging-deploy.sh re-verifies the downloaded package, requires the manifest-bound package identity and target/.htaccess, uses sshpass -d to provide the staging password to OpenSSH and SCP without placing it in command arguments, and emits only non-secret repository, commit, release, digest and URL evidence. It uploads the public package to a remote temporary directory, installs it, and performs one HTTPS /health check. It does not receive or create database credential files and does not perform reset, migration, provisioning, Genesis or platform opening. The current Development Staging v1 path does not perform activation, symlink switching, rollback choreography, capability probing, remote manifest reconstruction, root smoke checks, or session-directory changes. Installation or health failures return a nonzero result and trigger temporary local cleanup.

Staging provisioning contract

STAGING uses php bin/provision-target.php --database=staging from the internal staging provisioning artifact. The operator must set IDY_TARGET_DB_HOST, IDY_TARGET_DB_PORT, IDY_TARGET_DB_NAME, IDY_TARGET_DB_USER and IDY_TARGET_DB_PASS through the internal execution environment. IDY_STAGING_DB_NAME is optional when the configured target database name is itself used as the STAGING identity; when supplied, it must match that name. The resulting identity must be safe, must not equal idio2plat, and must not contain a production/live identity token. The command verifies the selected PDO database, requires it to be empty, applies migrations, runs foundation-only Genesis, installs the approved vocabulary prerequisites and applies the immutable platform-opening release/effect mapping. It never copies production data or credentials and creates no human User, Business or continuing authority.

Production provisioning is deferred for the current development phase and has no active selector in this command. The generic target runtime and production identity guard remain internal safety abstractions for future work. The STAGING provisioning artifact is separate from the public runtime package, contains no secrets and is used only by internal scripting.

bin/reset-staging-database.php --database=staging is the dedicated reset boundary for a disposable staging rebuild. It is CLI-only and requires explicit staging context, explicit reset confirmation, the staging selector, matching IDY_TARGET_DB_NAME and IDY_STAGING_DB_NAME, and a non-production staging identity. It obtains reset authority only from the separately named IDY_STAGING_RESET_DB_HOST, IDY_STAGING_RESET_DB_PORT, IDY_STAGING_RESET_DB_USER and IDY_STAGING_RESET_DB_PASS settings. The reset endpoint must match the configured staging database host and port; the reset user may be the same as the staging runtime/provisioning user under the agreed single-user staging model. Missing capability, mismatched identity or failed recreated-database verification stops the operation before it reports success. These settings must be supplied through the protected internal execution environment; they are not part of the GitHub deployment workflow, and no credential values are present in the repository.

The 2026-09-19 activation/staging investigation confirmed that the rotated local identity file is present and resolves to the documented host, but the host still rejects non-interactive SSH authentication with Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password) before any remote command. This is an infrastructure authorization failure, not evidence that symlink or rename activation is safe. Do not enable activation until the deployment account's accepted authentication mechanism is restored and the remote capability probe is completed without touching the active release or persistent state.

The removed legacy crypto helper used OpenSSL AES-256-GCM. It creates a random 12-byte IV and encodes IV || 16-byte authentication tag || ciphertext using Base64. Decryption checks encoding/length and the authentication result; there is no embedded key identifier or algorithm-version envelope. Both WordPress application passwords and TOTP secrets use this helper. APP_ENCRYPTION_KEY_VERSION is exposed by the WordPress service as metadata; Crypto does not select historical keys using it. Simply changing the key can make stored secrets unreadable. Define rotation, revocation, recovery and key custody before operational use. Keep key material outside MySQL, outside the web root and outside source control; manage its backup separately from database backups.

Libsodium sodium_crypto_secretbox was a proposed alternative, not the implementation. Any cipher/key-format change requires separate approval, threat analysis, explicit format/version handling and verification; these docs do not implement it or require legacy-data compatibility. Credential plaintext should exist only where needed in request memory, never in markup, logs, exception messages, discovery output or AI prompts. Review the actual error paths for redaction: a stated policy alone does not prove every existing exception is safe.

Authentication and Session Controls

The removed legacy security helper set session cookies with HttpOnly, SameSite=Strict, path /, session lifetime and conditional Secure. HTTPS detection uses the server HTTPS flag or X-Forwarded-Proto; proxy-header trust must be verified at the deployment boundary. Do not accept arbitrary forwarded headers from untrusted callers. Verify secure-cookie behavior on the real ingress topology. The removed legacy authentication controller verified password hashes, used a dummy hash for unknown users, regenerated the session ID on successful password authentication and gated MFA completion. The old login and MFA paths lock an existing account after five failed attempts for 900 seconds; this is not general distributed brute-force protection. The removed legacy profile controller hashed replacement passwords with bcrypt and required at least 14 characters. Reauthentication, confirmation, password/session revocation and account recovery behavior require separate review; a visible password form does not establish those guarantees.

The removed legacy TOTP helper implemented six-digit SHA-1 TOTP with 30-second periods and a default plus/minus-one-step verification window. The removed legacy two-factor controller stored the TOTP secret encrypted, generated eight recovery codes, stored their password hashes and marked a matching code used. Enrollment and pending verification restrict navigation through the front controller; this code does not decide future enrollment policy or mandate removal of PHP sessions. Verify expired challenges, secret exposure during enrollment, replay, rate limiting, recovery-code atomic consumption and concurrent enrollment/verification before relying on MFA assurance.

Request and Data Boundaries

Uploads and Safe Rendering

Escape untrusted text for its output context; use a designated sanitizer for intentionally rich content rather than treating all strings as trusted HTML. The removed legacy sanitizer removed selected dangerous HTML elements, event-handler attributes and selected unsafe URL schemes. It is not a complete Gutenberg schema validator or proof that every HTML/CSS/vector attack is covered. The removed legacy SVG sanitizer was used by Brand uploads; any future sanitizer needs its own allowlist and adversarial fixtures.

The old Brand upload handler permits JPEG, PNG, GIF, WebP and sanitized SVG, checks server-detected MIME and uploaded-file status, and limits files to 5 MiB. It generates random names and removes a new file after write/database failure; replacement cleanup and storage authorization still need review. Profile avatars also have a 5 MiB limit and server MIME checks, but only raster formats; naming/storage/cleanup differ from Brand. Do not silently promote either upload handler into a general file service or treat a guessed public URL as an access-control mechanism. New storage needs nonexecutable upload locations, authorized reads where required, safe filenames, content/disposition headers and lifecycle cleanup. Test active SVG, polyglots, malformed files, path traversal, oversize requests, mismatched MIME and unauthorized replacement/deletion.

User profile images follow the same protected media boundary. Store only an addressable Media/Asset reference on the User object (legacy Type name Principal); keep bytes outside the public document root, generate storage names server-side, validate detected type and size, and serve only an authorized safe rendition. Replacement/removal must clean up unreferenced bytes without deleting a still-referenced asset. Image metadata and access checks must not disclose storage paths, credentials or unrelated Business data. The existing legacy avatar handler is evidence to review, not a compliant new-platform implementation.

Current response headers include nosniff, frame denial, a referrer policy, permissions restrictions and CSP. The CSP still permits inline scripts/styles and named CDNs; header presence is not complete XSS protection. HSTS is conditional on the server HTTPS flag, so test HTTPS/proxy behavior and required external assets instead of claiming blanket coverage.

Old-Schema Setup

The removed legacy setup command was CLI-only and executed the removed old schema. Its former invocation was php bin/setup.php, from a trusted shell for a deliberately approved new, empty OLD-APPLICATION database only. It is not a new-platform bootstrap, upgrade runner, migration scheme, test fixture reset or safe command against an arbitrary configured database. The script does not establish that an existing database is empty; the operator must confirm the target before execution. No setup or database command is part of this documentation change. The removed initial-admin and password-reset scripts were old operational utilities, not specifications for new identity provisioning.

Deployment and Recovery

Use the chosen PHP/MariaDB runtime and verify required extensions, ingress, storage permissions, backups and service management before release. This is implementation verification, not another D07 approval gate. Use least-privilege database and remote credentials; verify TLS, web-root exclusions and environment availability to both web and any future worker process. On Plesk/Apache, verify directory-index precedence: a host's placeholder index.html can shadow the target index.php. Authorization-header forwarding may be needed for WordPress application passwords; Apache CGIPassAuth depends on host configuration and can cause site-wide failures when disallowed. Have the site operator validate an appropriate rewrite/server configuration on a controlled target; do not prescribe an untested global hosting edit. Restrict outbound/inbound integration access where operationally feasible, including known Idiosyncrasy egress IPs on a WordPress site where appropriate. Document database/blob backup retention, independent key custody, restore authorization and restoration drills; a backup without recoverable keys may be unusable. Record failure correlation, health checks, sanitized error logs and incident ownership without copying secret values into support evidence. There is no general worker/module runtime to deploy or monitor today; worker claiming, retries, health, shutdown and failed-work handling depend on Workflows and automation.

Verification Before Release

Use isolated fixtures for session/CSRF failures, cross-scope access, encryption tampering, missing keys, recovery-code reuse, uploads and sanitization. For WordPress, test HTTPS enforcement, private/reserved addresses, DNS changes, IPv6, redirects, timeouts and credential redaction; see WordPress security. Record checks, environment, date and limitations in status. A focused PHPUnit suite is available through bin/run-tests.ps1; it refreshes its disposable database from staging, so staging remains authoritative and read-only during normal verification. The roadmap fixes the first milestone and Spearmint scenario. Live WordPress, provider AI and a broad connector estate are not first-milestone prerequisites.

Initial Runtime Controls

Settled semantic contract: authoritative identity/context, permission and Policy enforcement, coherent history and explicit recovery. Chosen implementation mechanisms: the bounded controls below. Current status: this is the operational baseline, not per-control evidence; some bounded controls are implemented, while broader controls remain specified, unverified or not security-certified. See implementation status.

Definition publication authority configuration

The initial publisher is the already-established, active project-owner User. The minimum binding is the non-secret target configuration value IDY_TARGET_DEFINITION_PUBLISHER_PRINCIPAL_ID (legacy identifier for the User UUID), containing that User's exact UUIDv4. Parse it through the existing TargetRuntimeConfig environment configuration path. This identifies an existing User; it must never create one or be inferred from an Explorer allowlist, registration order, email address, generated identity or manifest. Missing, malformed, unresolved or inactive identity fails closed. The configured UUID is configuration, not a credential; do not treat it as proof of a human request or print it unnecessarily.

The existing TargetProvisioningRunner owns normal platform installation, but its initial run() path requires an empty database and occurs before a human User exists. Therefore the grant and Policy are installed by a separate explicit post-owner configuration invocation owned by this same provisioning boundary, after the User has been established through ordinary registration/confirmation. The invocation must verify the selected target/staging database identity and that the platform foundation/opening is current before changing state; serialize through the existing runtime coordination boundary; and use the existing platform AuthorityGrant/Policy descriptors and generic Object repository. It installs only the configured User's direct platform grant for definition.publish on the universal Definition Type and the matching active platform Policy. Stable object identity and exact matching existing values make the step idempotent; partial, conflicting or mismatched state fails closed.

This is a normal installation/configuration operation for an existing User, not a bootstrap protocol or self-authorising CLI. It creates no User or credential; it is not part of Genesis, the immutable platform-opening artifact, onboarding, HTTP startup or authority reassignment. The protected target configuration supplies the UUID, while the internal provisioning invocation performs the installation. Publisher reassignment/revocation and a general authority-management Operation remain deferred. No separate publication store, schema or migration is required by this binding.

Do not run setup, bootstrap, password provisioning, restore or deployment as a documentation check. Runtime evidence must include the relevant invariant tests, not just successful login rendering.