Right now clickhouse allows to create TTL columns for Distributed tables during alters. It is working normally until server restart. After that it won't start because engine distributed doesn't support TTL clause. Modify metadata is required.

My cluster has 21.3 but tested it also with 21.11 and the same behavior.
It is blocked during create but it should be also be blocked during alters.
Tested on table from tutorial
ENGINE = Distributed('stage, 'default', 'hits_v1') 
alter table hits_v1_all modify column UTMCampaign String TTL EventDate + INTERVAL 1 MONTH ← it passes normally
bool supportsDynamicSubcolumns() const override { return true; }
    NamesAndTypesList getVirtuals() const override;
    bool mayBenefitFromIndexForIn(const ASTPtr & left_in_operand, ContextPtr, const StorageMetadataPtr & metadata_snapshot) const override;
    
    
    struct SnapshotData : public StorageSnapshot::Data
    {
        DataPartsVector parts;
    };
    StorageSnapshotPtr getStorageSnapshot(const StorageMetadataPtr & metadata_snapshot, ContextPtr query_context) const override;
    
    void loadDataParts(bool skip_sanity_checks);
    String getLogName() const { return log_name; }
    Int64 getMaxBlockNumber() const;
    
    DataParts getDataParts(const DataPartStates & affordable_states) const;
    DataPartsVector getDataPartsVectorForInternalUsage(
        const DataPartStates & affordable_states,
        const DataPartsLock & lock,
        DataPartStateVector * out_states = nullptr,
        bool require_projection_parts = false) const;
    
    
    DataPartsVector getDataPartsVectorForInternalUsage(
        const DataPartStates & affordable_states, DataPartStateVector * out_states = nullptr, bool require_projection_parts = false) const;
    
    DataPartsVector getAllDataPartsVector(
        DataPartStateVector * out_states = nullptr,
        bool require_projection_parts = false) const;