Skip to content

Commit

Permalink
Move salt and config into cloud init model
Browse files Browse the repository at this point in the history
  • Loading branch information
joakimk committed Dec 11, 2024
1 parent 261f474 commit d1c1523
Show file tree
Hide file tree
Showing 12 changed files with 62 additions and 46 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,11 @@ A build is unlocked by posting to `/api/build/unlock` with the following attribu

## Cloud-init support

The app can be used to provision new servers using cloud-init. You can create a new script in the WEB UI which is then served by the API. In addition to this there is a rake task `rake app:cloud_init_login[remote_ip]` for accessing login credentials to the servers provisioned by the cloud-init script.
The app can be used to provision new servers using cloud-init. You can create a new script in the WEB UI which is then served by the API.

There is no example of how this script looks at this time, but you can use official docs and use variables in the template to access to public methods and config on CloudInitTemplateHelper.
In addition to this there is a rake task `rake app:cloud_init_password[template_name, remote_ip]` for accessing password for the servers provisioned by the cloud-init script (given you have used {{password}} in the template).

For more information see the [cloud-init docs](https://cloudinit.readthedocs.io/en/latest/).

## ENVs

Expand Down
8 changes: 4 additions & 4 deletions app/controllers/api/cloud_inits_controller.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
class Api::CloudInitsController < ApiController
def show
template = CloudInit.find_by!(name: params[:name]).data
cloud_init = CloudInit.find_by!(name: params[:name])

helper = CloudInitTemplateHelper.new(request.remote_ip)
helper = CloudInitTemplateHelper.new(cloud_init:, remote_ip: request.remote_ip)

# We don't run any code in the template itself since we don't need to
# and it removes one possible attack vector.
data = template.gsub(/{{(.*?)}}/) {
data = cloud_init.template.gsub(/{{(.*?)}}/) {
variable = $1

if helper.public_methods.include?(variable.to_sym)
if [ :password ].include?(variable.to_sym)
helper.public_send(variable)
else
helper.config(variable)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/cloud_inits_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ def setup_menu
end

def cloud_init_params
params.require(:cloud_init).permit(:name, :data)
params.require(:cloud_init).permit(:name, :template)
end
end
8 changes: 7 additions & 1 deletion app/models/cloud_init.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
class CloudInit < ActiveRecord::Base
validates :name, :data, presence: true
validates :name, :template, presence: true

before_validation :generate_password_salt, on: :create

def generate_password_salt
self.password_salt = SecureRandom.hex(32)
end
end
22 changes: 6 additions & 16 deletions app/models/cloud_init_template_helper.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
class CloudInitTemplateHelper
pattr_initialize :remote_ip
pattr_initialize [ :cloud_init!, :remote_ip! ]

def config(name)
name = name.to_s.upcase

if Rails.env.test? || Rails.env.development?
"test-config-#{name}"
else
ENV["CLOUD_INIT_CONFIG_#{name}"] || raise("Missing ENV: CLOUD_INIT_CONFIG_#{name}")
end
end

def username
config(:username)
cloud_init.config.fetch(name)
end

def password
password_salt =
secret =
if Rails.env.test? || Rails.env.development?
"test-salt"
"test-secret"
else
ENV.fetch("CLOUD_INIT_PASSWORD_SALT")
ENV.fetch("CLOUD_INIT_PASSWORD_SECRET")
end

combined = "#{password_salt}:#{remote_ip}"
combined = "#{secret}:#{cloud_init.password_salt}:#{remote_ip}"
hash = Digest::SHA256.hexdigest(combined)
hash.first(32)
end
Expand Down
8 changes: 4 additions & 4 deletions app/views/cloud_inits/_form.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
= bootstrap_form_for(cloud_init) do |f|
= f.text_field :name

= f.label :data, "Content"
= f.label :template, "Content"
p
strong NOTE: Keep a backup copy of this since it does not store old versions or handle if the pipeline session times out before you save.
#editor style="width: 90%; height: 1200px;" = f.object.data
#editor style="width: 90%; height: 1200px;" = f.object.template

= f.text_area :data, id: "cloud_init_data", style: (Rails.env.test? ? "" : "display: none;")
= f.text_area :template, id: "cloud_init_template", style: (Rails.env.test? ? "" : "display: none;")
script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ace.js"
script
| var editor = ace.edit("editor");
| editor.setTheme("ace/theme/monokai");
| editor.session.setMode("ace/mode/yaml");
| editor.setFontSize(16);
| editor.session.on("change", function() {
| document.getElementById("cloud_init_data").value = editor.getValue();
| document.getElementById("cloud_init_template").value = editor.getValue();
| });

p = link_to "Cloud-init docs", "https://cloudinit.readthedocs.io/en/latest/", target: "_blank"
Expand Down
2 changes: 1 addition & 1 deletion app/views/cloud_inits/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ h1 Cloud-inits
pre = "#include\n" + api_cloud_init_url(name: cloud_init.name, token: App.api_token)

p Contents:
pre = cloud_init.data
pre = cloud_init.template
p = link_to "New cloud-init", new_cloud_init_path
14 changes: 14 additions & 0 deletions db/migrate/20241210133507_change_cloud_inits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class ChangeCloudInits < ActiveRecord::Migration[7.1]
def change
rename_column :cloud_inits, :data, :template
add_column :cloud_inits, :password_salt, :string
add_column :cloud_inits, :config, :jsonb, null: false, default: {}

CloudInit.reset_column_information
CloudInit.all.each do |cloud_init|
cloud_init.update_column(:password_salt, SecureRandom.hex(32))
end

change_column :cloud_inits, :password_salt, :string, null: false
end
end
6 changes: 4 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_12_03_104945) do
ActiveRecord::Schema[7.1].define(version: 2024_12_10_133507) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand All @@ -25,9 +25,11 @@

create_table "cloud_inits", force: :cascade do |t|
t.string "name", null: false
t.text "data", null: false
t.text "template", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "password_salt", null: false
t.jsonb "config", default: {}, null: false
end

create_table "projects", force: :cascade do |t|
Expand Down
13 changes: 8 additions & 5 deletions lib/tasks/app.rake
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ namespace :app do
end
end

desc "Show the username and password for a cloud-init server"
task :cloud_init_login, [:remote_ip] => :environment do |_t, args|
desc "Show the password for a cloud-init server"
task :cloud_init_password, [:name, :remote_ip] => :environment do |_t, args|
name = args[:name]
remote_ip = args[:remote_ip]
helper = CloudInitTemplateHelper.new(remote_ip)
puts "Cloud init server #{remote_ip} has username \"#{helper.username}\" and password \"#{helper.password}\" (if the template uses them)"
end
cloud_init = CloudInit.find_by!(name: name)
helper = CloudInitTemplateHelper.new(remote_ip: remote_ip, cloud_init:)

# intentionally print it in a easily parsable format since people might build tooling around this
puts "template=#{name} ip=#{remote_ip} password=#{helper.password}"
end
end
2 changes: 1 addition & 1 deletion spec/features/cloud_inits_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
expect(current_path).to eq(cloud_inits_path)
expect(page).to have_content("Cloud-init was successfully created.")
cloud_init = CloudInit.first!
expect(cloud_init.data).to eq("content")
expect(cloud_init.template).to eq("content")

click_link "Edit"
fill_in "Name", with: "bar"
Expand Down
17 changes: 8 additions & 9 deletions spec/requests/api/cloud_inits_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@

RSpec.describe "GET /api/cloud_init", type: :request do
it "gets a cloud-init config if you have the right api token" do
cloud_init = CloudInit.create!(name: "foo", data: %{
cloud_init = CloudInit.create!(name: "foo", template: %{
#cloud-config
username: {{username}}
password: {{password}}
extra: {{extra}}
})
}, config: { "extra" => "test-config-extra" })
allow(App).to receive(:api_token).and_return("secret")

get "/api/cloud_init?token=secret&name=foo"

password = Digest::SHA256.hexdigest("test-salt:127.0.0.1").first(32)
expect(CloudInitTemplateHelper.new("127.0.0.1").password).to eq(password)
expect(cloud_init.password_salt.size).to eq(64)
password = Digest::SHA256.hexdigest("test-secret:#{cloud_init.password_salt}:127.0.0.1").first(32)
expect(CloudInitTemplateHelper.new(cloud_init:, remote_ip:"127.0.0.1").password).to eq(password)

expect(response).to be_successful
expect(response.body).to include("#cloud-config")
expect(response.body).to include("username: test-config-USERNAME")
expect(response.body).to include("password: #{password}\n")
expect(response.body).to include("extra: test-config-EXTRA\n")
expect(response.body).to include("extra: test-config-extra\n")
end

it "fails when the api token is wrong" do
cloud_init = CloudInit.create!(name: "foo", data: "#cloud-config...")
cloud_init = CloudInit.create!(name: "foo", template: "#cloud-config...")
allow(App).to receive(:api_token).and_return("secret")

get "/api/cloud_init?token=wrong&name=foo"
Expand All @@ -33,7 +32,7 @@
end

it "fails when the name is unknown" do
cloud_init = CloudInit.create!(name: "foo", data: "#cloud-config...")
cloud_init = CloudInit.create!(name: "foo", template: "#cloud-config...")
allow(App).to receive(:api_token).and_return("secret")

get "/api/cloud_init?token=wrong&name=bar"
Expand Down

0 comments on commit d1c1523

Please sign in to comment.