Appearance
clamd.conf — the generated configuration
The antivirus daemon does not get a hand-written configuration file. clamd.conf is rendered from the GrydFiles:Scanner section of the application's own appsettings.json, which is the single source for every limit the engine enforces.
Why it is generated
There is no command in the clamd protocol that asks the daemon what it is configured with. VERSION answers with the engine and the signature database; that is all. So a check that compares a profile ceiling against a StreamMaxLength mirrored in appsettings attests a value that may not be the running daemon's — the file could have been edited, or the container could be running an older copy, and the check would still pass.
One source, rendered into the daemon's file, is the only arrangement where the check means anything. The versioned artefact is therefore a template, not a configuration:
- Template:
src/Modules/Files/GrydFiles.Infrastructure.ClamAv/Configuration/clamd.conf.template - Renderer:
ClamdConfigurationRendererin the same project
Every number in the template is a placeholder, and a test strips the comments and the placeholders and fails if a single digit is left behind.
Rendering it
bash
dotnet run --project tools/GrydFiles.DevHost -- \
--settings templates/gryd-api/appsettings.json \
--output tools/GrydFiles.DevHost/generated/clamd.confThe renderer binds the GrydFiles section, applies the same post-configuration the application does and runs the same GrydFilesOptionsValidator before writing anything. A section that would bring the application down produces no file and exits non-zero — the compose never stands up a clamd configured with limits the application would refuse. The rendered file is then mounted read-only at /etc/clamav/clamd.conf.
The parameters
Everything below is closed by D13 and D14. None of it is left for the developer to pick, because every one of them, set wrong, produces either a silent failure or a refusal that looks like the user's fault.
GrydFiles:Scanner key | Value | Lands in | Why this value |
|---|---|---|---|
StreamMaxLengthBytes | 67108864 (64 MiB) | clamd.conf → StreamMaxLength | About 2.5× the largest profile ceiling in use (26 214 400 bytes = 25 MiB). The clamd default is 25M — exactly that ceiling, with zero slack for the 4 bytes of framing INSTREAM adds per chunk, so a file at the profile's maximum size comes back SizeLimitExceeded intermittently. Equal is not enough. |
MaxFileSizeBytes | 67108864 (64 MiB) | clamd.conf → MaxFileSize | Ceiling for the largest single member extracted from an archive, kept equal to StreamMaxLength. The one parameter where erring low releases content instead of refusing it: below what is needed the engine does not reject the archive — it skips the member and answers OK. |
MaxScanSizeBytes | 419430400 (400 MiB) | clamd.conf → MaxScanSize | Total examined across every member of an archive; lets a 25 MiB zip expand roughly 16× before the limit bites. Same silent failure as MaxFileSize if it is set short. |
MaxRecursion | 16 | clamd.conf → MaxRecursion | Nesting depth allowed inside archives — zip-bomb protection. Blowing the limit is an Error, and a file that blows it is never released — but only because the limit alert below is on. |
MaxFiles | 10000 | clamd.conf → MaxFiles | How many files may be extracted from one archive. Same protection, same treatment. |
ConcurrentDatabaseReload | true | clamd.conf → ConcurrentDatabaseReload | Keeps scanning while the signature database is refreshed. It is this option that demands the 4 GiB of container memory: the reload holds two copies of the database at once. |
AlertOnExceededLimits | true | clamd.conf → AlertExceedsMax | The parameter that makes the four limits above mean anything. Off — clamd's own default — an archive that blows MaxRecursion, MaxFiles, MaxFileSize or MaxScanSize is not refused: the engine stops unpacking, examines nothing further and answers stream: OK. The zip-bomb protection then protects the daemon and releases the file. On, the same archive comes back as Heuristics.Limits.Exceeded, which the reply parser turns into Error — not Infected, because the file is not malware, it is one the engine could not finish reading. |
TimeoutSeconds | 120 | the application (D14) | Fits a 25 MiB archive comfortably in the worst case and releases the Hangfire worker before the queue starts piling up. A timeout is not a verdict and never releases the file. |
RetryDelays | 00:01:00, 00:05:00, 00:15:00 | the application (D14) | Three attempts across about twenty-one minutes, which absorb a container restart and a database reload without holding the file for the whole afternoon. |
UseIdSession | false | the client, per call | IDSESSION reuses one connection for several files. On the single-file path the cost of opening and closing the session beats the saving; turn it on only where there is real batch processing. |
The limits only bite because AlertExceedsMax is on
This is worth stating on its own, because it is the one place where a sensible-looking configuration releases content. MaxRecursion, MaxFiles, MaxFileSize and MaxScanSize tell clamd how much of an archive to unpack. They do not tell it what to do when the archive asks for more, and clamd's default is to stop unpacking and answer stream: OK.
Measured against clamd 1.4.6 with the rendered configuration and AlertExceedsMax off: a 24-level nested archive and an archive of 12 000 members both came back clean. The limits were doing their job — they kept the daemon from exhausting itself — and the file went on to be released unexamined.
With AlertExceedsMax yes the same archives come back as Heuristics.Limits.Exceeded.MaxRecursion and Heuristics.Limits.Exceeded.MaxFiles. The reply parser maps that family to Error rather than Infected: the file is not malware and does not belong in quarantine, it is a file the engine could not finish reading, and an Error leaves it unreleased and retryable.
What is deliberately absent from clamd.conf
TimeoutSeconds, RetryDelays and UseIdSession are not clamd directives. The first two are the application's own patience and the third is a protocol command the client either sends or does not. Writing them into the daemon's file would invent configuration that clamd silently ignores — a setting that looks effective and is not. A test asserts that none of the three is rendered.
The daemon's shape — TCP port, local socket, database directory, the user it drops to — comes from ClamdDaemonOptions and not from GrydFiles:Scanner. Those are claims about one container; the limits are claims about files, and only the limits are validated against the profile ceilings.
LocalSocket deserves its own note: the official image's entrypoint polls for the socket file to decide that clamd came up. A configuration without it does not fail loudly — the container spins out CLAMD_STARTUP_TIMEOUT and then reports a failure that names nothing useful.
The sizes are written in bytes
The rendered file carries 67108864, not 64M. The byte count is what the application validated the profile ceilings against, and a rounded suffix would put a different number in front of the daemon than the one the check passed on.
Fail-fast on an incoherent section
GrydFilesOptionsValidator brings the application down at startup when any profile declares a MaxSizeBytes above StreamMaxLengthBytes, naming the profile and both values. It is not a warning in a log nobody read: a ceiling above what INSTREAM accepts does not refuse the upload, it lets a legitimate file at the profile's maximum size come back SizeLimitExceeded, and the operator reads a configuration error as the module rejecting the user's file.
The same rule runs inside the renderer, so an incoherent section cannot even produce a clamd.conf.
The contract with this page
scripts/check-doc-contracts.sh reads the GrydFiles:Scanner section of templates/gryd-api/appsettings.json, the table above and the template, and fails when they disagree: every key must be documented with its effective value, the template may not carry a number of its own, and the three client-side parameters may not appear as placeholders in it.
The decisions behind this page are in ADR 0010; the contract is in the specification.