Vettel led from start to finish in Montreal but was demoted to second by a penalty picked up after running wide at the first chicane and almost colliding with Hamilton as he rejoined.
Ferrari was in contention to end its winless start to the campaign after Vettel kept Hamilton at arm's length through the opening stint, but came under increasing pressure in the second half of the race.
Hamilton dropped to almost five seconds behind Vettel by virtue of extending his first stint by a couple of laps, but had stronger pace on the hard tyres and quickly caught the Ferrari.
However, as the two started lap 48 Hamilton was closer to Vettel again, and Vettel took to the grass at the first chicane after briefly losing the rear of his car on corner entry.
Leclerc ended up one second behind Vettel in the final classification, with Ferrari's first double podium finish of the season scant consolation for the Italian team.