Skip to content

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: ClamdConfigurationRenderer in 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.conf

The 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 keyValueLands inWhy this value
StreamMaxLengthBytes67108864 (64 MiB)clamd.conf → StreamMaxLengthAbout 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.
MaxFileSizeBytes67108864 (64 MiB)clamd.conf → MaxFileSizeCeiling 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.
MaxScanSizeBytes419430400 (400 MiB)clamd.conf → MaxScanSizeTotal 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.
MaxRecursion16clamd.conf → MaxRecursionNesting 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.
MaxFiles10000clamd.conf → MaxFilesHow many files may be extracted from one archive. Same protection, same treatment.
ConcurrentDatabaseReloadtrueclamd.conf → ConcurrentDatabaseReloadKeeps 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.
AlertOnExceededLimitstrueclamd.conf → AlertExceedsMaxThe 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.
TimeoutSeconds120the 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.
RetryDelays00:01:00, 00:05:00, 00:15:00the 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.
UseIdSessionfalsethe client, per callIDSESSION 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.

Released under the MIT License.