The left-handed Karunaratne registered his eighth century in his 50th Test.
Rabada, who came back after an injury, returned figures of 4-50, while Shamsi took three wickets on a seemingly turning sub-continent pitch. Rabada had made the first breakthrough for South Africa, dismissing Danushka Gunathilaka with a delivery that rose sharply and got the batsman caught behind for 26.
He then struck twice in his sixth over of the day, removing Angelo Mathews, for one, and Roshen Silva, for nought, shortly after lunch. Those two wickets came towards the start of Sri Lanka's middle-order, when they lost six wickets for 61 runs, slipping from a relatively comfortable 115-2.
But Karunaratne rarely attempted to counterattack, preferring instead to steadily build his innings, which featured 13 fours and an impressive six off Dale Steyn in the first session.
He completed his ton with a boundary off Shamsi and then moved to 150 for the fourth time in his Test career. South Africa skipper Faf du Plessis was forced to rotate his regular bowling options as Karunaratne played sensible cricket with the tail-enders.
Stand-in-captain Suranga Lakmal also fought with Karunaratne during their 48-run ninth wicket stand that frustrated the bowling side. Sri Lanka's regular captain Dinesh Chandimal is suspended for the two-match series, after pleading guilty to breaching the spirit of cricket during the team's Test tour of West Indies last month.