I solved this by adding this to my custom block twig:
{{ dump(content.field_footer_cta_buttons) }}
Which should've been like the first thing that I did.
I then saw that the structure actully looks like:
^ array:20 [▼
"#theme" => "field"
"#title" => "Footer CTA Buttons"
"#label_display" => "hidden"
"#view_mode" => "full"
"#language" => "en"
"#field_name" => "field_footer_cta_buttons"
"#field_type" => "link"
"#field_translatable" => false
"#entity_type" => "block_content"
"#bundle" => "footer_block"
"#object" => Drupal\block_content\Entity\BlockContent {#2804 ▶}
"#items" => Drupal\Core\Field\FieldItemList {#3601 ▶}
"#formatter" => "link"
"#is_multiple" => true
"#third_party_settings" => []
0 => array:4 [▼
"#type" => "link"
"#title" => "Some Link Text"
"#options" => array:1 [▶]
"#url" => Drupal\Core\Url {#3583 ▶}
]
1 => array:4 [▼
"#type" => "link"
"#title" => "Some Other Link Text"
"#options" => array:1 [▶]
"#url" => Drupal\Core\Url {#3118 ▶}
]
2 => array:4 [▼
"#type" => "link"
"#title" => "Yet More Link Text"
"#options" => array:1 [▶]
"#url" => Drupal\Core\Url {#3685 ▶}
]
"#cache" => array:3 [▶]
"#weight" => 2
]
To me this seems unintuitive and not conducive to iteration which I would think would be a common requirement. However, I'm betting someone way more knowledgeable than myself designed this so there's likely a very good reason for it and it's also likely that my approach is fundamentally flawed in some way.
My ultimate solution was:
{% for key in content.field_footer_cta_buttons|keys %}
{% if key == 0 or key == 1 or key == 2 %}
<a class='some-class' href='{{content.field_footer_cta_buttons[key]['#url']}}'>{{ content.field_footer_cta_buttons[key]['#title'] }}</a>
{% endif %}
{% endfor %}
I do realize this assumes the link field will always have a maximum of 3 values. So, something like:
{% if key matches '/^\\d+$/' %}
might be better than:
{% if key == 0 or key == 1 or key == 2 %}
If someone posts a better answer, I'll happily accept it.
Thank you.